# HG changeset patch # User Chris Cannam # Date 1410435902 -3600 # Node ID fb9a134672536f194e476cbe2a6e89618dad7ff0 # Parent d98d22a98252dcb7c4ffc417728959ad302b3fab# Parent 82fac3dcf4668a4e9bb91110581bc822a50fcfbd Merge from branch redmine-2.5-integration diff -r d98d22a98252 -r fb9a13467253 .gitignore --- a/.gitignore Wed May 07 14:15:02 2014 +0100 +++ b/.gitignore Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .hgignore --- a/.hgignore Wed May 07 14:15:02 2014 +0100 +++ b/.hgignore Thu Sep 11 12:45:02 2014 +0100 @@ -2,6 +2,8 @@ .project .loadpath +.powrc +.rvmrc config/additional_environment.rb config/configuration.yml config/database.yml diff -r d98d22a98252 -r fb9a13467253 .svn/entries --- a/.svn/entries Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -12 diff -r d98d22a98252 -r fb9a13467253 .svn/format --- a/.svn/format Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -12 diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/00205ce9aaa0cc98a4089319a62880b29732ab31.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00205ce9aaa0cc98a4089319a62880b29732ab31.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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): + # @param string :unit User measure unit. Possible values are:
A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit. + # @param mixed :format The format used for pages. It can be either one of the following values (case insensitive) or a custom format in the form of a two-element array containing the width and the height (expressed in the unit given by unit). + # @param boolean :unicode TRUE means that the input text is unicode (default = true) + # @param String :encoding charset encoding; default is UTF-8 + # + def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, encoding = "UTF-8") + + # Set internal character encoding to ASCII# + #FIXME 2007-05-25 (EJM) Level=0 - + # if (respond_to?("mb_internal_encoding") and mb_internal_encoding()) + # @internal_encoding = mb_internal_encoding(); + # mb_internal_encoding("ASCII"); + # } + + #Some checks + dochecks(); + + #Initialization of properties + @barcode ||= false + @buffer ||= '' + @diffs ||= [] + @color_flag ||= false + @default_table_columns ||= 4 + @table_columns ||= 0 + @max_table_columns ||= [] + @tr_id ||= 0 + @max_td_page ||= [] + @max_td_y ||= [] + @t_columns ||= 0 + @default_font ||= "FreeSans" if unicode + @default_font ||= "Helvetica" + @draw_color ||= '0 G' + @encoding ||= "UTF-8" + @fill_color ||= '0 g' + @fonts ||= {} + @font_family ||= '' + @font_files ||= {} + @font_style ||= '' + @font_size ||= 12 + @font_size_pt ||= 12 + @header_width ||= 0 + @header_logo ||= "" + @header_logo_width ||= 30 + @header_title ||= "" + @header_string ||= "" + @images ||= {} + @img_scale ||= 1 + @in_footer ||= false + @is_unicode = unicode + @lasth ||= 0 + @links ||= [] + @list_ordered ||= [] + @list_count ||= [] + @li_spacer ||= "" + @li_count ||= 0 + @spacer ||= "" + @quote_count ||= 0 + @prevquote_count ||= 0 + @quote_top ||= [] + @quote_page ||= [] + @n ||= 2 + @offsets ||= [] + @orientation_changes ||= [] + @page ||= 0 + @page_links ||= {} + @pages ||= [] + @pdf_version ||= "1.3" + @prevfill_color ||= [255,255,255] + @prevtext_color ||= [0,0,0] + @print_header ||= false + @print_footer ||= false + @state ||= 0 + @tableborder ||= 0 + @tdbegin ||= false + @tdtext ||= '' + @tdwidth ||= 0 + @tdheight ||= 0 + @tdalign ||= "L" + @tdfill ||= 0 + @tempfontsize ||= 10 + @text_color ||= '0 g' + @underline ||= false + @deleted ||= false + @ws ||= 0 + + #Standard Unicode fonts + @core_fonts = { + 'courier'=>'Courier', + 'courierB'=>'Courier-Bold', + 'courierI'=>'Courier-Oblique', + 'courierBI'=>'Courier-BoldOblique', + 'helvetica'=>'Helvetica', + 'helveticaB'=>'Helvetica-Bold', + 'helveticaI'=>'Helvetica-Oblique', + 'helveticaBI'=>'Helvetica-BoldOblique', + 'times'=>'Times-Roman', + 'timesB'=>'Times-Bold', + 'timesI'=>'Times-Italic', + 'timesBI'=>'Times-BoldItalic', + 'symbol'=>'Symbol', + 'zapfdingbats'=>'ZapfDingbats'} + + #Scale factor + case unit.downcase + when 'pt' ; @k=1 + when 'mm' ; @k=72/25.4 + when 'cm' ; @k=72/2.54 + when 'in' ; @k=72 + else Error("Incorrect unit: #{unit}") + end + + #Page format + if format.is_a?(String) + # Page formats (45 standard ISO paper formats and 4 american common formats). + # Paper cordinates are calculated in this way: (inches# 72) where (1 inch = 2.54 cm) + case (format.upcase) + when '4A0' ; format = [4767.87,6740.79] + when '2A0' ; format = [3370.39,4767.87] + when 'A0' ; format = [2383.94,3370.39] + when 'A1' ; format = [1683.78,2383.94] + when 'A2' ; format = [1190.55,1683.78] + when 'A3' ; format = [841.89,1190.55] + when 'A4' ; format = [595.28,841.89] # ; default + when 'A5' ; format = [419.53,595.28] + when 'A6' ; format = [297.64,419.53] + when 'A7' ; format = [209.76,297.64] + when 'A8' ; format = [147.40,209.76] + when 'A9' ; format = [104.88,147.40] + when 'A10' ; format = [73.70,104.88] + when 'B0' ; format = [2834.65,4008.19] + when 'B1' ; format = [2004.09,2834.65] + when 'B2' ; format = [1417.32,2004.09] + when 'B3' ; format = [1000.63,1417.32] + when 'B4' ; format = [708.66,1000.63] + when 'B5' ; format = [498.90,708.66] + when 'B6' ; format = [354.33,498.90] + when 'B7' ; format = [249.45,354.33] + when 'B8' ; format = [175.75,249.45] + when 'B9' ; format = [124.72,175.75] + when 'B10' ; format = [87.87,124.72] + when 'C0' ; format = [2599.37,3676.54] + when 'C1' ; format = [1836.85,2599.37] + when 'C2' ; format = [1298.27,1836.85] + when 'C3' ; format = [918.43,1298.27] + when 'C4' ; format = [649.13,918.43] + when 'C5' ; format = [459.21,649.13] + when 'C6' ; format = [323.15,459.21] + when 'C7' ; format = [229.61,323.15] + when 'C8' ; format = [161.57,229.61] + when 'C9' ; format = [113.39,161.57] + when 'C10' ; format = [79.37,113.39] + when 'RA0' ; format = [2437.80,3458.27] + when 'RA1' ; format = [1729.13,2437.80] + when 'RA2' ; format = [1218.90,1729.13] + when 'RA3' ; format = [864.57,1218.90] + when 'RA4' ; format = [609.45,864.57] + when 'SRA0' ; format = [2551.18,3628.35] + when 'SRA1' ; format = [1814.17,2551.18] + when 'SRA2' ; format = [1275.59,1814.17] + when 'SRA3' ; format = [907.09,1275.59] + when 'SRA4' ; format = [637.80,907.09] + when 'LETTER' ; format = [612.00,792.00] + when 'LEGAL' ; format = [612.00,1008.00] + when 'EXECUTIVE' ; format = [521.86,756.00] + when 'FOLIO' ; format = [612.00,936.00] + #else then Error("Unknown page format: #{format}" + end + @fw_pt = format[0] + @fh_pt = format[1] + else + @fw_pt = format[0]*@k + @fh_pt = format[1]*@k + end + + @fw = @fw_pt/@k + @fh = @fh_pt/@k + + #Page orientation + orientation = orientation.downcase + if orientation == 'p' or orientation == 'portrait' + @def_orientation = 'P' + @w_pt = @fw_pt + @h_pt = @fh_pt + elsif orientation == 'l' or orientation == 'landscape' + @def_orientation = 'L' + @w_pt = @fh_pt + @h_pt = @fw_pt + else + Error("Incorrect orientation: #{orientation}") + end + + @fw = @w_pt/@k + @fh = @h_pt/@k + + @cur_orientation = @def_orientation + @w = @w_pt/@k + @h = @h_pt/@k + #Page margins (1 cm) + margin = 28.35/@k + SetMargins(margin, margin) + #Interior cell margin (1 mm) + @c_margin = margin / 10 + #Line width (0.2 mm) + @line_width = 0.567 / @k + #Automatic page break + SetAutoPageBreak(true, 2 * margin) + #Full width display mode + SetDisplayMode('fullwidth') + #Compression + SetCompression(true) + #Set default PDF version number + @pdf_version = "1.3" + + @encoding = encoding + @b = 0 + @i = 0 + @u = 0 + @href = '' + @fontlist = ["arial", "times", "courier", "helvetica", "symbol"] + @issetfont = false + @issetcolor = false + + SetFillColor(200, 200, 200, true) + SetTextColor(0, 0, 0, true) + end + + # + # Set the image scale. + # @param float :scale image scale. + # @author Nicola Asuni + # @since 1.5.2 + # + def SetImageScale(scale) + @img_scale = scale; + end + alias_method :set_image_scale, :SetImageScale + + # + # Returns the image scale. + # @return float image scale. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetImageScale() + return @img_scale; + end + alias_method :get_image_scale, :GetImageScale + + # + # Returns the page width in units. + # @return int page width. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetPageWidth() + return @w; + end + alias_method :get_page_width, :GetPageWidth + + # + # Returns the page height in units. + # @return int page height. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetPageHeight() + return @h; + end + alias_method :get_page_height, :GetPageHeight + + # + # Returns the page break margin. + # @return int page break margin. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetBreakMargin() + return @b_margin; + end + alias_method :get_break_margin, :GetBreakMargin + + # + # Returns the scale factor (number of points in user unit). + # @return int scale factor. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetScaleFactor() + return @k; + end + alias_method :get_scale_factor, :GetScaleFactor + + # + # Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change them. + # @param float :left Left margin. + # @param float :top Top margin. + # @param float :right Right margin. Default value is the left one. + # @since 1.0 + # @see SetLeftMargin(), SetTopMargin(), SetRightMargin(), SetAutoPageBreak() + # + def SetMargins(left, top, right=-1) + #Set left, top and right margins + @l_margin = left + @t_margin = top + if (right == -1) + right = left + end + @r_margin = right + end + alias_method :set_margins, :SetMargins + + # + # Defines the left margin. The method can be called before creating the first page. If the current abscissa gets out of page, it is brought back to the margin. + # @param float :margin The margin. + # @since 1.4 + # @see SetTopMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() + # + def SetLeftMargin(margin) + #Set left margin + @l_margin = margin + if ((@page>0) and (@x < margin)) + @x = margin + end + end + alias_method :set_left_margin, :SetLeftMargin + + # + # Defines the top margin. The method can be called before creating the first page. + # @param float :margin The margin. + # @since 1.5 + # @see SetLeftMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() + # + def SetTopMargin(margin) + #Set top margin + @t_margin = margin + end + alias_method :set_top_margin, :SetTopMargin + + # + # Defines the right margin. The method can be called before creating the first page. + # @param float :margin The margin. + # @since 1.5 + # @see SetLeftMargin(), SetTopMargin(), SetAutoPageBreak(), SetMargins() + # + def SetRightMargin(margin) + #Set right margin + @r_margin = margin + end + alias_method :set_right_margin, :SetRightMargin + + # + # Enables or disables the automatic page breaking mode. When enabling, the second parameter is the distance from the bottom of the page that defines the triggering limit. By default, the mode is on and the margin is 2 cm. + # @param boolean :auto Boolean indicating if mode should be on or off. + # @param float :margin Distance from the bottom of the page. + # @since 1.0 + # @see Cell(), MultiCell(), AcceptPageBreak() + # + def SetAutoPageBreak(auto, margin=0) + #Set auto page break mode and triggering margin + @auto_page_break = auto + @b_margin = margin + @page_break_trigger = @h - margin + end + alias_method :set_auto_page_break, :SetAutoPageBreak + + # + # Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a specific zooming factor or use viewer default (configured in the Preferences menu of Acrobat). The page layout can be specified too: single at once, continuous display, two columns or viewer default. By default, documents use the full width mode with continuous display. + # @param mixed :zoom The zoom to use. It can be one of the following string values or a number indicating the zooming factor to use. + # @param string :layout The page layout. Possible values are: + # @since 1.2 + # + def SetDisplayMode(zoom, layout = 'continuous') + #Set display mode in viewer + if (zoom == 'fullpage' or zoom == 'fullwidth' or zoom == 'real' or zoom == 'default' or !zoom.is_a?(String)) + @zoom_mode = zoom + else + Error("Incorrect zoom display mode: #{zoom}") + end + if (layout == 'single' or layout == 'continuous' or layout == 'two' or layout == 'default') + @layout_mode = layout + else + Error("Incorrect layout display mode: #{layout}") + end + end + alias_method :set_display_mode, :SetDisplayMode + + # + # Activates or deactivates page compression. When activated, the internal representation of each page is compressed, which leads to a compression ratio of about 2 for the resulting document. Compression is on by default. + # Note: the Zlib extension is required for this feature. If not present, compression will be turned off. + # @param boolean :compress Boolean indicating if compression must be enabled. + # @since 1.4 + # + def SetCompression(compress) + #Set page compression + if (respond_to?('gzcompress')) + @compress = compress + else + @compress = false + end + end + alias_method :set_compression, :SetCompression + + # + # Defines the title of the document. + # @param string :title The title. + # @since 1.2 + # @see SetAuthor(), SetCreator(), SetKeywords(), SetSubject() + # + def SetTitle(title) + #Title of document + @title = title + end + alias_method :set_title, :SetTitle + + # + # Defines the subject of the document. + # @param string :subject The subject. + # @since 1.2 + # @see SetAuthor(), SetCreator(), SetKeywords(), SetTitle() + # + def SetSubject(subject) + #Subject of document + @subject = subject + end + alias_method :set_subject, :SetSubject + + # + # Defines the author of the document. + # @param string :author The name of the author. + # @since 1.2 + # @see SetCreator(), SetKeywords(), SetSubject(), SetTitle() + # + def SetAuthor(author) + #Author of document + @author = author + end + alias_method :set_author, :SetAuthor + + # + # Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'. + # @param string :keywords The list of keywords. + # @since 1.2 + # @see SetAuthor(), SetCreator(), SetSubject(), SetTitle() + # + def SetKeywords(keywords) + #Keywords of document + @keywords = keywords + end + alias_method :set_keywords, :SetKeywords + + # + # Defines the creator of the document. This is typically the name of the application that generates the PDF. + # @param string :creator The name of the creator. + # @since 1.2 + # @see SetAuthor(), SetKeywords(), SetSubject(), SetTitle() + # + def SetCreator(creator) + #Creator of document + @creator = creator + end + alias_method :set_creator, :SetCreator + + # + # Defines an alias for the total number of pages. It will be substituted as the document is closed.
+ # Example:
+ #
+	# 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): The default value is the one passed to the constructor. + # @since 1.0 + # @see TCPDF(), Header(), Footer(), SetMargins() + # + def AddPage(orientation='') + #Start a new page + if (@state==0) + Open(); + end + family=@font_family; + style=@font_style + (@underline ? 'U' : '') + (@deleted ? 'D' : ''); + size=@font_size_pt; + lw=@line_width; + dc=@draw_color; + fc=@fill_color; + tc=@text_color; + cf=@color_flag; + if (@page>0) + #Page footer + @in_footer=true; + Footer(); + @in_footer=false; + #Close page + endpage(); + end + #Start new page + beginpage(orientation); + #Set line cap style to square + out('2 J'); + #Set line width + @line_width = lw; + out(sprintf('%.2f w', lw*@k)); + #Set font + if (family) + SetFont(family, style, size); + end + #Set colors + @draw_color = dc; + if (dc!='0 G') + out(dc); + end + @fill_color = fc; + if (fc!='0 g') + out(fc); + end + @text_color = tc; + @color_flag = cf; + #Page header + Header(); + #Restore line width + if (@line_width != lw) + @line_width = lw; + out(sprintf('%.2f w', lw*@k)); + end + #Restore font + if (family) + SetFont(family, style, size); + end + #Restore colors + if (@draw_color != dc) + @draw_color = dc; + out(dc); + end + if (@fill_color != fc) + @fill_color = fc; + out(fc); + end + @text_color = tc; + @color_flag = cf; + end + alias_method :add_page, :AddPage + + # + # Rotate object. + # @param float :angle angle in degrees for counter-clockwise rotation + # @param int :x abscissa of the rotation center. Default is current x position + # @param int :y ordinate of the rotation center. Default is current y position + # + def Rotate(angle, x="", y="") + + if (x == '') + x = @x; + end + + if (y == '') + y = @y; + end + + if (@rtl) + x = @w - x; + angle = -@angle; + end + + y = (@h - y) * @k; + x *= @k; + + # calculate elements of transformation matrix + tm = [] + tm[0] = ::Math::cos(deg2rad(angle)); + tm[1] = ::Math::sin(deg2rad(angle)); + tm[2] = -tm[1]; + tm[3] = tm[0]; + tm[4] = x + tm[1] * y - tm[0] * x; + tm[5] = y - tm[0] * y - tm[1] * x; + + # generate the transformation matrix + Transform(tm); + end + alias_method :rotate, :Rotate + + # + # Starts a 2D tranformation saving current graphic state. + # This function must be called before scaling, mirroring, translation, rotation and skewing. + # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. + # + def StartTransform + out('q'); + end + alias_method :start_transform, :StartTransform + + # + # Stops a 2D tranformation restoring previous graphic state. + # This function must be called after scaling, mirroring, translation, rotation and skewing. + # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. + # + def StopTransform + out('Q'); + end + alias_method :stop_transform, :StopTransform + + # + # Apply graphic transformations. + # @since 2.1.000 (2008-01-07) + # @see StartTransform(), StopTransform() + # + def Transform(tm) + x = out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', tm[0], tm[1], tm[2], tm[3], tm[4], tm[5])); + end + alias_method :transform, :Transform + + # + # Set header data. + # @param string :ln header image logo + # @param string :lw header image logo width in mm + # @param string :ht string to print as title on document header + # @param string :hs string to print on document header + # + def SetHeaderData(ln="", lw=0, ht="", hs="") + @header_logo = ln || "" + @header_logo_width = lw || 0 + @header_title = ht || "" + @header_string = hs || "" + end + alias_method :set_header_data, :SetHeaderData + + # + # Set header margin. + # (minimum distance between header and top page margin) + # @param int :hm distance in millimeters + # + def SetHeaderMargin(hm=10) + @header_margin = hm; + end + alias_method :set_header_margin, :SetHeaderMargin + + # + # Set footer margin. + # (minimum distance between footer and bottom page margin) + # @param int :fm distance in millimeters + # + def SetFooterMargin(fm=10) + @footer_margin = fm; + end + alias_method :set_footer_margin, :SetFooterMargin + + # + # Set a flag to print page header. + # @param boolean :val set to true to print the page header (default), false otherwise. + # + def SetPrintHeader(val=true) + @print_header = val; + end + alias_method :set_print_header, :SetPrintHeader + + # + # Set a flag to print page footer. + # @param boolean :value set to true to print the page footer (default), false otherwise. + # + def SetPrintFooter(val=true) + @print_footer = val; + end + alias_method :set_print_footer, :SetPrintFooter + + # + # This method is used to render the page header. + # It is automatically called by AddPage() and could be overwritten in your own inherited class. + # + def Header() + if (@print_header) + if (@original_l_margin.nil?) + @original_l_margin = @l_margin; + end + if (@original_r_margin.nil?) + @original_r_margin = @r_margin; + end + + #set current position + SetXY(@original_l_margin, @header_margin); + + if ((@header_logo) and (@header_logo != @@k_blank_image)) + Image(@header_logo, @original_l_margin, @header_margin, @header_logo_width); + else + @img_rb_y = GetY(); + end + + cell_height = ((@@k_cell_height_ratio * @header_font[2]) / @k).round(2) + + header_x = @original_l_margin + (@header_logo_width * 1.05); #set left margin for text data cell + + # header title + SetFont(@header_font[0], 'B', @header_font[2] + 1); + SetX(header_x); + Cell(@header_width, cell_height, @header_title, 0, 1, 'L'); + + # header string + SetFont(@header_font[0], @header_font[1], @header_font[2]); + SetX(header_x); + MultiCell(@header_width, cell_height, @header_string, 0, 'L', 0); + + # print an ending header line + if (@header_width) + #set style for cell border + SetLineWidth(0.3); + SetDrawColor(0, 0, 0); + SetY(1 + (@img_rb_y > GetY() ? @img_rb_y : GetY())); + SetX(@original_l_margin); + Cell(0, 0, '', 'T', 0, 'C'); + end + + #restore position + SetXY(@original_l_margin, @t_margin); + end + end + alias_method :header, :Header + + # + # This method is used to render the page footer. + # It is automatically called by AddPage() and could be overwritten in your own inherited class. + # + def Footer() + if (@print_footer) + + if (@original_l_margin.nil?) + @original_l_margin = @l_margin; + end + if (@original_r_margin.nil?) + @original_r_margin = @r_margin; + end + + #set font + SetFont(@footer_font[0], @footer_font[1] , @footer_font[2]); + #set style for cell border + line_width = 0.3; + SetLineWidth(line_width); + SetDrawColor(0, 0, 0); + + footer_height = ((@@k_cell_height_ratio * @footer_font[2]) / @k).round; #footer height, was , 2) + #get footer y position + footer_y = @h - @footer_margin - footer_height; + #set current position + SetXY(@original_l_margin, footer_y); + + #print document barcode + if (@barcode) + Ln(); + barcode_width = ((@w - @original_l_margin - @original_r_margin)).round; #max width + writeBarcode(@original_l_margin, footer_y + line_width, barcode_width, footer_height - line_width, "C128B", false, false, 2, @barcode); + end + + SetXY(@original_l_margin, footer_y); + + #Print page number + Cell(0, footer_height, @l['w_page'] + " " + PageNo().to_s + ' / {nb}', 'T', 0, 'R'); + end + end + alias_method :footer, :Footer + + # + # Returns the current page number. + # @return int page number + # @since 1.0 + # @see alias_nb_pages() + # + def PageNo() + #Get current page number + return @page; + end + alias_method :page_no, :PageNo + + # + # Defines the color used for all drawing operations (lines, rectangles and cell borders). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. + # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 + # @param int :g Green component (between 0 and 255) + # @param int :b Blue component (between 0 and 255) + # @since 1.3 + # @see SetFillColor(), SetTextColor(), Line(), Rect(), Cell(), MultiCell() + # + def SetDrawColor(r, g=-1, b=-1) + #Set color for all stroking operations + if ((r==0 and g==0 and b==0) or g==-1) + @draw_color=sprintf('%.3f G', r/255.0); + else + @draw_color=sprintf('%.3f %.3f %.3f RG', r/255.0, g/255.0, b/255.0); + end + if (@page>0) + out(@draw_color); + end + end + alias_method :set_draw_color, :SetDrawColor + + # + # Defines the color used for all filling operations (filled rectangles and cell backgrounds). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. + # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 + # @param int :g Green component (between 0 and 255) + # @param int :b Blue component (between 0 and 255) + # @param boolean :storeprev if true stores the RGB array on :prevfill_color variable. + # @since 1.3 + # @see SetDrawColor(), SetTextColor(), Rect(), Cell(), MultiCell() + # + def SetFillColor(r, g=-1, b=-1, storeprev=false) + #Set color for all filling operations + if ((r==0 and g==0 and b==0) or g==-1) + @fill_color=sprintf('%.3f g', r/255.0); + else + @fill_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); + end + @color_flag=(@fill_color!=@text_color); + if (@page>0) + out(@fill_color); + end + if (storeprev) + # store color as previous value + @prevfill_color = [r, g, b] + end + end + alias_method :set_fill_color, :SetFillColor + + # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors + def SetCmykFillColor(c, m, y, k, storeprev=false) + #Set color for all filling operations + @fill_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); + @color_flag=(@fill_color!=@text_color); + if (storeprev) + # store color as previous value + @prevtext_color = [c, m, y, k] + end + if (@page>0) + out(@fill_color); + end + end + alias_method :set_cmyk_fill_color, :SetCmykFillColor + + # + # Defines the color used for text. It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. + # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 + # @param int :g Green component (between 0 and 255) + # @param int :b Blue component (between 0 and 255) + # @param boolean :storeprev if true stores the RGB array on :prevtext_color variable. + # @since 1.3 + # @see SetDrawColor(), SetFillColor(), Text(), Cell(), MultiCell() + # + def SetTextColor(r, g=-1, b=-1, storeprev=false) + #Set color for text + if ((r==0 and :g==0 and :b==0) or :g==-1) + @text_color=sprintf('%.3f g', r/255.0); + else + @text_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); + end + @color_flag=(@fill_color!=@text_color); + if (storeprev) + # store color as previous value + @prevtext_color = [r, g, b] + end + end + alias_method :set_text_color, :SetTextColor + + # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors + def SetCmykTextColor(c, m, y, k, storeprev=false) + #Set color for text + @text_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); + @color_flag=(@fill_color!=@text_color); + if (storeprev) + # store color as previous value + @prevtext_color = [c, m, y, k] + end + end + alias_method :set_cmyk_text_color, :SetCmykTextColor + + # + # Returns the length of a string in user unit. A font must be selected.
+ # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02] + # @param string :s The string whose length is to be computed + # @return int + # @since 1.2 + # + def GetStringWidth(s) + #Get width of a string in the current font + s = s.to_s; + cw = @current_font['cw'] + w = 0; + if (@is_unicode) + unicode = UTF8StringToArray(s); + unicode.each do |char| + if (!cw[char].nil?) + w += cw[char]; + # This should not happen. UTF8StringToArray should guarentee the array is ascii values. + # elsif (c!cw[char[0]].nil?) + # w += cw[char[0]]; + # elsif (!cw[char.chr].nil?) + # w += cw[char.chr]; + elsif (!@current_font['desc']['MissingWidth'].nil?) + w += @current_font['desc']['MissingWidth']; # set default size + else + w += 500; + end + end + else + s.each_byte do |c| + if cw[c.chr] + w += cw[c.chr]; + elsif cw[?c.chr] + w += cw[?c.chr] + end + end + end + return (w * @font_size / 1000.0); + end + alias_method :get_string_width, :GetStringWidth + + # + # Defines the line width. By default, the value equals 0.2 mm. The method can be called before the first page is created and the value is retained from page to page. + # @param float :width The width. + # @since 1.0 + # @see Line(), Rect(), Cell(), MultiCell() + # + def SetLineWidth(width) + #Set line width + @line_width = width; + if (@page>0) + out(sprintf('%.2f w', width*@k)); + end + end + alias_method :set_line_width, :SetLineWidth + + # + # Draws a line between two points. + # @param float :x1 Abscissa of first point + # @param float :y1 Ordinate of first point + # @param float :x2 Abscissa of second point + # @param float :y2 Ordinate of second point + # @since 1.0 + # @see SetLineWidth(), SetDrawColor() + # + def Line(x1, y1, x2, y2) + #Draw a line + out(sprintf('%.2f %.2f m %.2f %.2f l S', x1 * @k, (@h - y1) * @k, x2 * @k, (@h - y2) * @k)); + end + alias_method :line, :Line + + def Circle(mid_x, mid_y, radius, style='') + mid_y = (@h-mid_y)*@k + out(sprintf("q\n")) # postscript content in pdf + # init line type etc. with /GSD gs G g (grey) RG rg (RGB) w=line witdh etc. + out(sprintf("1 j\n")) # line join + # translate ("move") circle to mid_y, mid_y + out(sprintf("1 0 0 1 %f %f cm", mid_x, mid_y)) + kappa = 0.5522847498307933984022516322796 + # Quadrant 1 + x_s = 0.0 # 12 o'clock + y_s = 0.0 + radius + x_e = 0.0 + radius # 3 o'clock + y_e = 0.0 + out(sprintf("%f %f m\n", x_s, y_s)) # move to 12 o'clock + # cubic bezier control point 1, start height and kappa * radius to the right + bx_e1 = x_s + (radius * kappa) + by_e1 = y_s + # cubic bezier control point 2, end and kappa * radius above + bx_e2 = x_e + by_e2 = y_e + (radius * kappa) + # draw cubic bezier from current point to x_e/y_e with bx_e1/by_e1 and bx_e2/by_e2 as bezier control points + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + # Quadrant 2 + x_s = x_e + y_s = y_e # 3 o'clock + x_e = 0.0 + y_e = 0.0 - radius # 6 o'clock + bx_e1 = x_s # cubic bezier point 1 + by_e1 = y_s - (radius * kappa) + bx_e2 = x_e + (radius * kappa) # cubic bezier point 2 + by_e2 = y_e + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + # Quadrant 3 + x_s = x_e + y_s = y_e # 6 o'clock + x_e = 0.0 - radius + y_e = 0.0 # 9 o'clock + bx_e1 = x_s - (radius * kappa) # cubic bezier point 1 + by_e1 = y_s + bx_e2 = x_e # cubic bezier point 2 + by_e2 = y_e - (radius * kappa) + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + # Quadrant 4 + x_s = x_e + y_s = y_e # 9 o'clock + x_e = 0.0 + y_e = 0.0 + radius # 12 o'clock + bx_e1 = x_s # cubic bezier point 1 + by_e1 = y_s + (radius * kappa) + bx_e2 = x_e - (radius * kappa) # cubic bezier point 2 + by_e2 = y_e + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + if style=='F' + op='f' + elsif style=='FD' or style=='DF' + op='b' + else + op='s' + end + out(sprintf("#{op}\n")) # stroke circle, do not fill and close path + # for filling etc. b, b*, f, f* + out(sprintf("Q\n")) # finish postscript in PDF + end + alias_method :circle, :Circle + + # + # Outputs a rectangle. It can be drawn (border only), filled (with no border) or both. + # @param float :x Abscissa of upper-left corner + # @param float :y Ordinate of upper-left corner + # @param float :w Width + # @param float :h Height + # @param string :style Style of rendering. Possible values are: + # @since 1.0 + # @see SetLineWidth(), SetDrawColor(), SetFillColor() + # + def Rect(x, y, w, h, style='') + #Draw a rectangle + if (style=='F') + op='f'; + elsif (style=='FD' or style=='DF') + op='B'; + else + op='S'; + end + out(sprintf('%.2f %.2f %.2f %.2f re %s', x * @k, (@h - y) * @k, w * @k, -h * @k, op)); + end + alias_method :rect, :Rect + + # + # Imports a TrueType or Type1 font and makes it available. It is necessary to generate a font definition file first with the makefont.rb utility. The definition file (and the font file itself when embedding) must be present either in the current directory or in the one indicated by FPDF_FONTPATH if the constant is defined. If it could not be found, the error "Could not include font definition file" is generated. + # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02]. + # Example:
+ #
+	# :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): + # @param string :file The font definition file. By default, the name is built from the family and style, in lower case with no space. + # @since 1.5 + # @see SetFont() + # + def AddFont(family, style='', file='') + if (family.empty?) + return; + end + + #Add a TrueType or Type1 font + family = family.downcase + if ((!@is_unicode) and (family == 'arial')) + family = 'helvetica'; + end + + style=style.upcase + style=style.gsub('U',''); + style=style.gsub('D',''); + if (style == 'IB') + style = 'BI'; + end + + fontkey = family + style; + # check if the font has been already added + if !@fonts[fontkey].nil? + return; + end + + if (file=='') + file = family.gsub(' ', '') + style.downcase + '.rb'; + end + font_file_name = getfontpath(file) + if (font_file_name.nil?) + # try to load the basic file without styles + file = family.gsub(' ', '') + '.rb'; + font_file_name = getfontpath(file) + end + if font_file_name.nil? + Error("Could not find font #{file}.") + end + require(getfontpath(file)) + font_desc = TCPDFFontDescriptor.font(file) + + if (font_desc[:name].nil? and @@fpdf_charwidths.nil?) + Error('Could not include font definition file'); + end + + i = @fonts.length+1; + if (@is_unicode) + @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg], 'cMap' => font_desc[:cMap], 'registry' => font_desc[:registry]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + else + @fonts[fontkey]={'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + end + + if (!font_desc[:diff].nil? and (!font_desc[:diff].empty?)) + #Search existing encodings + d=0; + nb=@diffs.length; + 1.upto(nb) do |i| + if (@diffs[i]== font_desc[:diff]) + d = i; + break; + end + end + if (d==0) + d = nb+1; + @diffs[d] = font_desc[:diff]; + end + @fonts[fontkey]['diff'] = d; + end + if (font_desc[:file] and font_desc[:file].length > 0) + if (font_desc[:type] == "TrueType") or (font_desc[:type] == "TrueTypeUnicode") + @font_files[font_desc[:file]] = {'length1' => font_desc[:originalsize]} + else + @font_files[font_desc[:file]] = {'length1' => font_desc[:size1], 'length2' => font_desc[:size2]} + end + end + end + alias_method :add_font, :AddFont + + # + # Sets the font used to print character strings. It is mandatory to call this method at least once before printing text or the resulting document would not be valid. + # The font can be either a standard one or a font added via the AddFont() method. Standard fonts use Windows encoding cp1252 (Western Europe). + # The method can be called before the first page is created and the font is retained from page to page. + # If you just wish to change the current font size, it is simpler to call SetFontSize(). + # Note: for the standard fonts, the font metric files must be accessible. There are three possibilities for this:
+ # Example for the last case (note the trailing slash):
+ #
+	# 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');
+	# 

+ # If the file corresponding to the requested font is not found, the error "Could not include font metric file" is generated. + # @param string :family Family font. It can be either a name defined by AddFont() or one of the standard families (case insensitive):It is also possible to pass an empty string. In that case, the current family is retained. + # @param string :style Font style. Possible values are (case insensitive):or any combination. The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats + # @param float :size Font size in points. The default value is the current size. If no size has been specified since the beginning of the document, the value taken is 12 + # @since 1.0 + # @see AddFont(), SetFontSize(), Cell(), MultiCell(), Write() + # + def SetFont(family, style='', size=0) + # save previous values + @prevfont_family = @font_family; + @prevfont_style = @font_style; + + family=family.downcase; + if (family=='') + family=@font_family; + end + if ((!@is_unicode) and (family == 'arial')) + family = 'helvetica'; + elsif ((family=="symbol") or (family=="zapfdingbats")) + style=''; + end + + style=style.upcase; + + if (style.include?('U')) + @underline=true; + style= style.gsub('U',''); + else + @underline=false; + end + if (style.include?('D')) + @deleted=true; + style= style.gsub('D',''); + else + @deleted=false; + end + if (style=='IB') + style='BI'; + end + if (size==0) + size=@font_size_pt; + end + + # try to add font (if not already added) + AddFont(family, style); + + #Test if font is already selected + if ((@font_family == family) and (@font_style == style) and (@font_size_pt == size)) + return; + end + + fontkey = family + style; + style = '' if (@fonts[fontkey].nil? and !@fonts[family].nil?) + + #Test if used for the first time + if (@fonts[fontkey].nil?) + #Check if one of the standard fonts + if (!@core_fonts[fontkey].nil?) + if @@fpdf_charwidths[fontkey].nil? + #Load metric file + file = family; + if ((family!='symbol') and (family!='zapfdingbats')) + file += style.downcase; + end + if (getfontpath(file + '.rb').nil?) + # try to load the basic file without styles + file = family; + fontkey = family; + end + require(getfontpath(file + '.rb')); + font_desc = TCPDFFontDescriptor.font(file) + if ((@is_unicode and ctg.nil?) or ((!@is_unicode) and (@@fpdf_charwidths[fontkey].nil?)) ) + Error("Could not include font metric file [" + fontkey + "]: " + getfontpath(file + ".rb")); + end + end + i = @fonts.length + 1; + + if (@is_unicode) + @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + else + @fonts[fontkey] = {'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + end + else + Error('Undefined font: ' + family + ' ' + style); + end + end + #Select it + @font_family = family; + @font_style = style; + @font_size_pt = size; + @font_size = size / @k; + @current_font = @fonts[fontkey]; # was & may need deep copy? + if (@page>0) + out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); + end + end + alias_method :set_font, :SetFont + + # + # Defines the size of the current font. + # @param float :size The size (in points) + # @since 1.0 + # @see SetFont() + # + def SetFontSize(size) + #Set font size in points + if (@font_size_pt== size) + return; + end + @font_size_pt = size; + @font_size = size.to_f / @k; + if (@page > 0) + out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); + end + end + alias_method :set_font_size, :SetFontSize + + # + # Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document.
+ # The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is defined with SetLink(). + # @since 1.5 + # @see Cell(), Write(), Image(), Link(), SetLink() + # + def AddLink() + #Create a new internal link + n=@links.length+1; + @links[n]=[0,0]; + return n; + end + alias_method :add_link, :AddLink + + # + # Defines the page and position a link points to + # @param int :link The link identifier returned by AddLink() + # @param float :y Ordinate of target position; -1 indicates the current position. The default value is 0 (top of page) + # @param int :page Number of target page; -1 indicates the current page. This is the default value + # @since 1.5 + # @see AddLink() + # + def SetLink(link, y=0, page=-1) + #Set destination of internal link + if (y==-1) + y=@y; + end + if (page==-1) + page=@page; + end + @links[link] = [page, y] + end + alias_method :set_link, :SetLink + + # + # Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), Write() or Image(), but this method can be useful for instance to define a clickable area inside an image. + # @param float :x Abscissa of the upper-left corner of the rectangle + # @param float :y Ordinate of the upper-left corner of the rectangle + # @param float :w Width of the rectangle + # @param float :h Height of the rectangle + # @param mixed :link URL or identifier returned by AddLink() + # @since 1.5 + # @see AddLink(), Cell(), Write(), Image() + # + def Link(x, y, w, h, link) + #Put a link on the page + @page_links ||= Array.new + @page_links[@page] ||= Array.new + @page_links[@page].push([x * @k, @h_pt - y * @k, w * @k, h*@k, link]); + end + alias_method :link, :Link + + # + # Prints a character string. The origin is on the left of the first charcter, on the baseline. This method allows to place a string precisely on the page, but it is usually easier to use Cell(), MultiCell() or Write() which are the standard methods to print text. + # @param float :x Abscissa of the origin + # @param float :y Ordinate of the origin + # @param string :txt String to print + # @since 1.0 + # @see SetFont(), SetTextColor(), Cell(), MultiCell(), Write() + # + def Text(x, y, txt) + #Output a string + s=sprintf('BT %.2f %.2f Td (%s) Tj ET', x * @k, (@h-y) * @k, escapetext(txt)); + if (@underline and (txt!='')) + s += ' ' + dolinetxt(x, y, txt); + end + if (@color_flag) + s='q ' + @text_color + ' ' + s + ' Q'; + end + out(s); + end + alias_method :text, :Text + + # + # Whenever a page break condition is met, the method is called, and the break is issued or not depending on the returned value. The default implementation returns a value according to the mode selected by SetAutoPageBreak().
+ # This method is called automatically and should not be called directly by the application.
+ # Example:
+ # The method is overriden in an inherited class in order to obtain a 3 column layout:
+ #
+	# 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.
+ # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. + # @param float :w Cell width. If 0, the cell extends up to the right margin. + # @param float :h Cell height. Default value: 0. + # @param string :txt String to print. Default value: empty string. + # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:or a string containing some or all of the following characters (in any order): + # @param int :ln Indicates where the current position should go after the call. Possible values are: + # Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. + # @param string :align Allows to center or align the text. Possible values are: + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @param mixed :link URL or identifier returned by AddLink(). + # @since 1.0 + # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak() + # + def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil) + #Output a cell + k=@k; + if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + x = @x; + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + @x = x; + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * k)); + end + else + @page += 1; + @y=@t_margin; + end + end + + if (w == 0) + w = @w - @r_margin - @x; + end + s = ''; + if ((fill.to_i == 1) or (border.to_i == 1)) + if (fill.to_i == 1) + op = (border.to_i == 1) ? 'B' : 'f'; + else + op = 'S'; + end + s = sprintf('%.2f %.2f %.2f %.2f re %s ', @x * k, (@h - @y) * k, w * k, -h * k, op); + end + if (border.is_a?(String)) + x=@x; + y=@y; + if (border.include?('L')) + s<0) + # Go to next line + @y += h; + if (ln == 1) + @x = @l_margin; + end + else + @x += w; + end + end + alias_method :cell, :Cell + + # + # This method allows printing text with line breaks. They can be automatic (as soon as the text reaches the right border of the cell) or explicit (via the \n character). As many cells as necessary are output, one below the other.
+ # Text can be aligned, centered or justified. The cell block can be framed and the background painted. + # @param float :w Width of cells. If 0, they extend up to the right margin of the page. + # @param float :h Height of cells. + # @param string :txt String to print + # @param mixed :border Indicates if borders must be drawn around the cell block. The value can be either a number:or a string containing some or all of the following characters (in any order): + # @param string :align Allows to center or align the text. Possible values are: + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @param int :ln Indicates where the current position should go after the call. Possible values are: + # @since 1.3 + # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), Cell(), Write(), SetAutoPageBreak() + # + def MultiCell(w, h, txt, border=0, align='J', fill=0, ln=1) + + # save current position + prevx = @x; + prevy = @y; + prevpage = @page; + + #Output text with automatic or explicit line breaks + + if (w == 0) + w = @w - @r_margin - @x; + end + + wmax = (w - 3 * @c_margin); + + s = txt.gsub("\r", ''); # remove carriage returns + nb = s.length; + + b=0; + if (border) + if (border==1) + border='LTRB'; + b='LRT'; + b2='LR'; + elsif border.is_a?(String) + b2=''; + if (border.include?('L')) + b2<<'L'; + end + if (border.include?('R')) + b2<<'R'; + end + b=(border.include?('T')) ? b2 + 'T' : b2; + end + end + sep=-1; + to_index=0; + from_j=0; + l=0; + ns=0; + nl=1; + + while to_index < nb + #Get next character + c = s[to_index]; + if c == "\n"[0] + #Explicit line break + if @ws > 0 + @ws = 0 + out('0 Tw') + end + #Ed Moss - change begin + end_i = to_index == 0 ? 0 : to_index - 1 + # Changed from s[from_j..to_index] to fix bug reported by Hans Allis. + from_j = to_index == 0 ? 1 : from_j + Cell(w, h, s[from_j..end_i], b, 2, align, fill) + #change end + to_index += 1 + sep=-1 + from_j=to_index + l=0 + ns=0 + nl += 1 + b = b2 if border and nl==2 + next + end + if (c == " "[0]) + sep = to_index; + ls = l; + ns += 1; + end + + l = GetStringWidth(s[from_j, to_index - from_j]); + + if (l > wmax) + #Automatic line break + if (sep == -1) + if (to_index == from_j) + to_index += 1; + end + if (@ws > 0) + @ws = 0; + out('0 Tw'); + end + Cell(w, h, s[from_j..to_index-1], b, 2, align, fill) # my FPDF version + else + if (align=='J' || align=='justify' || align=='justified') + @ws = (ns>1) ? (wmax-ls)/(ns-1) : 0; + out(sprintf('%.3f Tw', @ws * @k)); + end + Cell(w, h, s[from_j..sep], b, 2, align, fill); + to_index = sep + 1; + end + sep=-1; + from_j = to_index; + l=0; + ns=0; + nl += 1; + if (border and (nl==2)) + b = b2; + end + else + to_index += 1; + end + end + #Last chunk + if (@ws>0) + @ws=0; + out('0 Tw'); + end + if (border.is_a?(String) and border.include?('B')) + b<<'B'; + end + Cell(w, h, s[from_j, to_index-from_j], b, 2, align, fill); + + # move cursor to specified position + # since 2007-03-03 + if (ln == 1) + # go to the beginning of the next line + @x = @l_margin; + elsif (ln == 0) + # go to the top-right of the cell + @page = prevpage; + @y = prevy; + @x = prevx + w; + elsif (ln == 2) + # go to the bottom-left of the cell + @x = prevx; + end + end + alias_method :multi_cell, :MultiCell + + # + # This method prints text from the current position. When the right margin is reached (or the \n character is met) a line break occurs and text continues from the left margin. Upon method exit, the current position is left just at the end of the text. It is possible to put a link on the text.
+ # Example:
+ #
+	# #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 wmax) + #Automatic line break (word wrapping) + if (sep == -1) + if (@x > @l_margin) + #Move to next line + @x = @l_margin; + @y += h; + w=@w - @r_margin - @x; + wmax=(w - 3 * @c_margin); + i += 1 + nl += 1 + next + end + if (i == j) + i += 1 + end + Cell(w, h, s[j, (i-1)], 0, 2, '', fill, link); + else + Cell(w, h, s[j, (sep-j)], 0, 2, '', fill, link); + i = sep+1; + end + sep = -1; + j = i; + l = 0; + if (nl==1) + @x = @l_margin; + w = @w - @r_margin - @x; + wmax = (w - 3 * @c_margin); + end + nl += 1; + else + i += 1; + end + end + #Last chunk + if (i != j) + Cell(GetStringWidth(s[j..i]), h, s[j..i], 0, 0, '', fill, link); + end + end + alias_method :write, :Write + + # + # Puts an image in the page. The upper-left corner must be given. The dimensions can be specified in different ways: + # Supported formats are JPEG and PNG. + # For JPEG, all flavors are allowed: + # For PNG, are allowed: + # but are not supported: + # If a transparent color is defined, it will be taken into account (but will be only interpreted by Acrobat 4 and above).
+ # The format can be specified explicitly or inferred from the file extension.
+ # It is possible to put a link on the image.
+ # Remark: if an image is used several times, only one copy will be embedded in the file.
+ # @param string :file Name of the file containing the image. + # @param float :x Abscissa of the upper-left corner. + # @param float :y Ordinate of the upper-left corner. + # @param float :w Width of the image in the page. If not specified or equal to zero, it is automatically calculated. + # @param float :h Height of the image in the page. If not specified or equal to zero, it is automatically calculated. + # @param string :type Image format. Possible values are (case insensitive): JPG, JPEG, PNG. If not specified, the type is inferred from the file extension. + # @param mixed :link URL or identifier returned by AddLink(). + # @since 1.1 + # @see AddLink() + # + def Image(file, x, y, w=0, h=0, type='', link=nil) + #Put an image on the page + if (@images[file].nil?) + #First use of image, get info + if (type == '') + pos = File::basename(file).rindex('.'); + if (pos.nil? or pos == 0) + Error('Image file has no extension and no type was specified: ' + file); + end + pos = file.rindex('.'); + type = file[pos+1..-1]; + end + type.downcase! + if (type == 'jpg' or type == 'jpeg') + info=parsejpg(file); + elsif (type == 'png') + info=parsepng(file); + elsif (type == 'gif') + tmpFile = imageToPNG(file); + info=parsepng(tmpFile.path); + tmpFile.delete + else + #Allow for additional formats + mtd='parse' + type; + if (!self.respond_to?(mtd)) + Error('Unsupported image type: ' + type); + end + info=send(mtd, file); + end + info['i']=@images.length+1; + @images[file] = info; + else + info=@images[file]; + end + #Automatic width and height calculation if needed + if ((w == 0) and (h == 0)) + rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k)) + rescale_x = 1 if rescale_x >= 1 + if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * @k)); + end + else + @page += 1; + end + y=@t_margin; + end + rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k)) + rescale_y = 1 if rescale_y >= 1 + rescale = rescale_y >= rescale_x ? rescale_x : rescale_y + + #Put image at 72 dpi + # 2004-06-14 :: Nicola Asuni, scale factor where added + w = info['w'] * rescale / (@img_scale * @k); + h = info['h'] * rescale / (@img_scale * @k); + elsif (w == 0) + w = h * info['w'] / info['h']; + elsif (h == 0) + h = w * info['h'] / info['w']; + end + out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i'])); + if (link) + Link(x, y, w, h, link); + end + + #2002-07-31 - Nicola Asuni + # set right-bottom corner coordinates + @img_rb_x = x + w; + @img_rb_y = y + h; + end + alias_method :image, :Image + + # + # Performs a line break. The current abscissa goes back to the left margin and the ordinate increases by the amount passed in parameter. + # @param float :h The height of the break. By default, the value equals the height of the last printed cell. + # @since 1.0 + # @see Cell() + # + def Ln(h='') + #Line feed; default value is last cell height + @x=@l_margin; + if (h.is_a?(String)) + @y += @lasth; + else + @y += h; + end + + k=@k; + if (@y > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + x = @x; + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + @x = x; + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * k)); + end + else + @page += 1; + @y=@t_margin; + end + end + + end + alias_method :ln, :Ln + + # + # Returns the abscissa of the current position. + # @return float + # @since 1.2 + # @see SetX(), GetY(), SetY() + # + def GetX() + #Get x position + return @x; + end + alias_method :get_x, :GetX + + # + # Defines the abscissa of the current position. If the passed value is negative, it is relative to the right of the page. + # @param float :x The value of the abscissa. + # @since 1.2 + # @see GetX(), GetY(), SetY(), SetXY() + # + def SetX(x) + #Set x position + if (x>=0) + @x = x; + else + @x=@w+x; + end + end + alias_method :set_x, :SetX + + # + # Returns the ordinate of the current position. + # @return float + # @since 1.0 + # @see SetY(), GetX(), SetX() + # + def GetY() + #Get y position + return @y; + end + alias_method :get_y, :GetY + + # + # Moves the current abscissa back to the left margin and sets the ordinate. If the passed value is negative, it is relative to the bottom of the page. + # @param float :y The value of the ordinate. + # @since 1.0 + # @see GetX(), GetY(), SetY(), SetXY() + # + def SetY(y) + #Set y position and reset x + @x=@l_margin; + if (y>=0) + @y = y; + else + @y=@h+y; + end + end + alias_method :set_y, :SetY + + # + # Defines the abscissa and ordinate of the current position. If the passed values are negative, they are relative respectively to the right and bottom of the page. + # @param float :x The value of the abscissa + # @param float :y The value of the ordinate + # @since 1.2 + # @see SetX(), SetY() + # + def SetXY(x, y) + #Set x and y positions + SetY(y); + SetX(x); + end + alias_method :set_xy, :SetXY + + # + # Send the document to a given destination: string, local file or browser. In the last case, the plug-in may be used (if present) or a download ("Save as" dialog box) may be forced.
+ # The method first calls Close() if necessary to terminate the document. + # @param string :name The name of the file. If not given, the document will be sent to the browser (destination I) with the name doc.pdf. + # @param string :dest Destination where to send the document. It can take one of the following values:If the parameter is not specified but a name is given, destination is F. If no parameter is specified at all, destination is I.
+ # @since 1.0 + # @see Close() + # + def Output(name='', dest='') + #Output PDF to some destination + #Finish document if necessary + if (@state < 3) + Close(); + end + #Normalize parameters + # Boolean no longer supported + # if (dest.is_a?(Boolean)) + # dest = dest ? 'D' : 'F'; + # end + dest = dest.upcase + if (dest=='') + if (name=='') + name='doc.pdf'; + dest='I'; + else + dest='F'; + end + end + case (dest) + when 'I' + # This is PHP specific code + ##Send to standard output + # if (ob_get_contents()) + # Error('Some data has already been output, can\'t send PDF file'); + # end + # if (php_sapi_name()!='cli') + # #We send to a browser + # header('Content-Type: application/pdf'); + # if (headers_sent()) + # Error('Some data has already been output to browser, can\'t send PDF file'); + # end + # header('Content-Length: ' + @buffer.length); + # header('Content-disposition: inline; filename="' + name + '"'); + # end + return @buffer; + + when 'D' + # PHP specific + #Download file + # if (ob_get_contents()) + # Error('Some data has already been output, can\'t send PDF file'); + # end + # if (!_SERVER['HTTP_USER_AGENT'].nil? && SERVER['HTTP_USER_AGENT'].include?('MSIE')) + # header('Content-Type: application/force-download'); + # else + # header('Content-Type: application/octet-stream'); + # end + # if (headers_sent()) + # Error('Some data has already been output to browser, can\'t send PDF file'); + # end + # header('Content-Length: '+ @buffer.length); + # header('Content-disposition: attachment; filename="' + name + '"'); + return @buffer; + + when 'F' + open(name,'wb') do |f| + f.write(@buffer) + end + # PHP code + # #Save to local file + # f=open(name,'wb'); + # if (!f) + # Error('Unable to create output file: ' + name); + # end + # fwrite(f,@buffer,@buffer.length); + # f.close + + when 'S' + #Return as a string + return @buffer; + else + Error('Incorrect output destination: ' + dest); + + end + return ''; + end + alias_method :output, :Output + + # Protected methods + + # + # Check for locale-related bug + # @access protected + # + def dochecks() + #Check for locale-related bug + if (1.1==1) + Error('Don\'t alter the locale before including class file'); + end + #Check for decimal separator + if (sprintf('%.1f',1.0)!='1.0') + setlocale(LC_NUMERIC,'C'); + end + end + + # + # Return fonts path + # @access protected + # + def getfontpath(file) + # Is it in the @@font_path? + if @@font_path + fpath = File.join @@font_path, file + if File.exists?(fpath) + return fpath + end + end + # Is it in this plugin's font folder? + fpath = File.join File.dirname(__FILE__), 'fonts', file + if File.exists?(fpath) + return fpath + end + # Could not find it. + nil + end + + # + # Start document + # @access protected + # + def begindoc() + #Start document + @state=1; + out('%PDF-1.3'); + end + + # + # putpages + # @access protected + # + def putpages() + nb = @page; + if (@alias_nb_pages) + nbstr = UTF8ToUTF16BE(nb.to_s, false); + #Replace number of pages + 1.upto(nb) do |n| + @pages[n].gsub!(@alias_nb_pages, nbstr) + end + end + if @def_orientation=='P' + w_pt=@fw_pt + h_pt=@fh_pt + else + w_pt=@fh_pt + h_pt=@fw_pt + end + filter=(@compress) ? '/Filter /FlateDecode ' : '' + 1.upto(nb) do |n| + #Page + newobj + out('<>>>'; + else + l=@links[pl[4]]; + h=!@orientation_changes[l[0]].nil? ? w_pt : h_pt; + annots<>',1+2*l[0], h-l[1]*@k); + end + end + out(annots + ']'); + end + out('/Contents ' + (@n+1).to_s + ' 0 R>>'); + out('endobj'); + #Page content + p=(@compress) ? gzcompress(@pages[n]) : @pages[n]; + newobj(); + out('<<' + filter + '/Length '+ p.length.to_s + '>>'); + putstream(p); + out('endobj'); + end + #Pages root + @offsets[1]=@buffer.length; + out('1 0 obj'); + out('<>'); + out('endobj'); + end + + # + # Adds fonts + # putfonts + # @access protected + # + def putfonts() + nf=@n; + @diffs.each do |diff| + #Encodings + newobj(); + out('<>'); + out('endobj'); + end + @font_files.each do |file, info| + #Font file embedding + newobj(); + @font_files[file]['n']=@n; + font=''; + open(getfontpath(file),'rb') do |f| + font = f.read(); + end + compressed=(file[-2,2]=='.z'); + if (!compressed && !info['length2'].nil?) + header=((font[0][0])==128); + if (header) + #Strip first binary header + font=font[6]; + end + if header && (font[info['length1']][0] == 128) + #Strip second binary header + font=font[0..info['length1']] + font[info['length1']+6]; + end + end + out('<>'); + open(getfontpath(file),'rb') do |f| + putstream(font) + end + out('endobj'); + end + @fonts.each do |k, font| + #Font objects + @fonts[k]['n']=@n+1; + type = font['type']; + name = font['name']; + if (type=='core') + #Standard font + newobj(); + out('<>'); + out('endobj'); + elsif type == 'Type0' + putType0(font) + elsif (type=='Type1' || type=='TrueType') + #Additional Type1 or TrueType font + newobj(); + out('<>'); + out('endobj'); + #Widths + newobj(); + cw=font['cw']; # & + s='['; + 32.upto(255) do |i| + s << cw[i.chr] + ' '; + end + out(s + ']'); + out('endobj'); + #Descriptor + newobj(); + s='<>'); + out('endobj'); + else + #Allow for additional types + mtd='put' + type.downcase; + if (!self.respond_to?(mtd)) + Error('Unsupported font type: ' + type) + else + self.send(mtd,font) + end + end + end + end + + def putType0(font) + #Type0 + newobj(); + out('<>') + out('endobj') + #CIDFont + newobj() + out('<>') + out('/FontDescriptor '+(@n+1).to_s+' 0 R') + w='/W [1 [' + font['cw'].keys.sort.each {|key| + w+=font['cw'][key].to_s + " " +# ActionController::Base::logger.debug key.to_s +# ActionController::Base::logger.debug font['cw'][key].to_s + } + out(w+'] 231 325 500 631 [500] 326 389 500]') + out('>>') + out('endobj') + #Font descriptor + newobj() + out('<>') + out('endobj') + end + + # + # putimages + # @access protected + # + def putimages() + filter=(@compress) ? '/Filter /FlateDecode ' : ''; + @images.each do |file, info| # was while(list(file, info)=each(@images)) + newobj(); + @images[file]['n']=@n; + out('<>'); + putstream(info['data']); + @images[file]['data']=nil + out('endobj'); + #Palette + if (info['cs']=='Indexed') + newobj(); + pal=(@compress) ? gzcompress(info['pal']) : info['pal']; + out('<<' + filter + '/Length ' + pal.length.to_s + '>>'); + putstream(pal); + out('endobj'); + end + end + end + + # + # putxobjectdict + # @access protected + # + def putxobjectdict() + @images.each_value do |image| + out('/I' + image['i'].to_s + ' ' + image['n'].to_s + ' 0 R'); + end + end + + # + # putresourcedict + # @access protected + # + def putresourcedict() + out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + out('/Font <<'); + @fonts.each_value do |font| + out('/F' + font['i'].to_s + ' ' + font['n'].to_s + ' 0 R'); + end + out('>>'); + out('/XObject <<'); + putxobjectdict(); + out('>>'); + end + + # + # putresources + # @access protected + # + def putresources() + putfonts(); + putimages(); + #Resource dictionary + @offsets[2]=@buffer.length; + out('2 0 obj'); + out('<<'); + putresourcedict(); + out('>>'); + out('endobj'); + end + + # + # putinfo + # @access protected + # + def putinfo() + out('/Producer ' + textstring(PDF_PRODUCER)); + if (!@title.nil?) + out('/Title ' + textstring(@title)); + end + if (!@subject.nil?) + out('/Subject ' + textstring(@subject)); + end + if (!@author.nil?) + out('/Author ' + textstring(@author)); + end + if (!@keywords.nil?) + out('/Keywords ' + textstring(@keywords)); + end + if (!@creator.nil?) + out('/Creator ' + textstring(@creator)); + end + out('/CreationDate ' + textstring('D:' + Time.now.strftime('%Y%m%d%H%M%S'))); + end + + # + # putcatalog + # @access protected + # + def putcatalog() + out('/Type /Catalog'); + out('/Pages 1 0 R'); + if (@zoom_mode=='fullpage') + out('/OpenAction [3 0 R /Fit]'); + elsif (@zoom_mode=='fullwidth') + out('/OpenAction [3 0 R /FitH null]'); + elsif (@zoom_mode=='real') + out('/OpenAction [3 0 R /XYZ null null 1]'); + elsif (!@zoom_mode.is_a?(String)) + out('/OpenAction [3 0 R /XYZ null null ' + (@zoom_mode/100) + ']'); + end + if (@layout_mode=='single') + out('/PageLayout /SinglePage'); + elsif (@layout_mode=='continuous') + out('/PageLayout /OneColumn'); + elsif (@layout_mode=='two') + out('/PageLayout /TwoColumnLeft'); + end + end + + # + # puttrailer + # @access protected + # + def puttrailer() + out('/Size ' + (@n+1).to_s); + out('/Root ' + @n.to_s + ' 0 R'); + out('/Info ' + (@n-1).to_s + ' 0 R'); + end + + # + # putheader + # @access protected + # + def putheader() + out('%PDF-' + @pdf_version); + end + + # + # enddoc + # @access protected + # + def enddoc() + putheader(); + putpages(); + putresources(); + #Info + newobj(); + out('<<'); + putinfo(); + out('>>'); + out('endobj'); + #Catalog + newobj(); + out('<<'); + putcatalog(); + out('>>'); + out('endobj'); + #Cross-ref + o=@buffer.length; + out('xref'); + out('0 ' + (@n+1).to_s); + out('0000000000 65535 f '); + 1.upto(@n) do |i| + out(sprintf('%010d 00000 n ',@offsets[i])); + end + #Trailer + out('trailer'); + out('<<'); + puttrailer(); + out('>>'); + out('startxref'); + out(o); + out('%%EOF'); + @state=3; + end + + # + # beginpage + # @access protected + # + def beginpage(orientation) + @page += 1; + @pages[@page]=''; + @state=2; + @x=@l_margin; + @y=@t_margin; + @font_family=''; + #Page orientation + if (orientation.empty?) + orientation=@def_orientation; + else + orientation.upcase! + if (orientation!=@def_orientation) + @orientation_changes[@page]=true; + end + end + if (orientation!=@cur_orientation) + #Change orientation + if (orientation=='P') + @w_pt=@fw_pt; + @h_pt=@fh_pt; + @w=@fw; + @h=@fh; + else + @w_pt=@fh_pt; + @h_pt=@fw_pt; + @w=@fh; + @h=@fw; + end + @page_break_trigger=@h-@b_margin; + @cur_orientation = orientation; + end + end + + # + # End of page contents + # @access protected + # + def endpage() + @state=1; + end + + # + # Begin a new object + # @access protected + # + def newobj() + @n += 1; + @offsets[@n]=@buffer.length; + out(@n.to_s + ' 0 obj'); + end + + # + # Underline and Deleted text + # @access protected + # + def dolinetxt(x, y, txt) + up = @current_font['up']; + ut = @current_font['ut']; + w = GetStringWidth(txt) + @ws * txt.count(' '); + sprintf('%.2f %.2f %.2f %.2f re f', x * @k, (@h - (y - up / 1000.0 * @font_size)) * @k, w * @k, -ut / 1000.0 * @font_size_pt); + end + + # + # Extract info from a JPEG file + # @access protected + # + def parsejpg(file) + a=getimagesize(file); + if (a.empty?) + Error('Missing or incorrect image file: ' + file); + end + if (!a[2].nil? and a[2]!='JPEG') + Error('Not a JPEG file: ' + file); + end + if (a['channels'].nil? or a['channels']==3) + colspace='DeviceRGB'; + elsif (a['channels']==4) + colspace='DeviceCMYK'; + else + colspace='DeviceGray'; + end + bpc=!a['bits'].nil? ? a['bits'] : 8; + #Read whole file + data=''; + + open(file,'rb') do |f| + data< a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data} + end + + def imageToPNG(file) + return unless Object.const_defined?(:Magick) + + img = Magick::ImageList.new(file) + img.format = 'PNG' # convert to PNG from gif + img.opacity = 0 # PNG alpha channel delete + + #use a temporary file.... + tmpFile = Tempfile.new(['', '_' + File::basename(file) + '.png'], @@k_path_cache); + tmpFile.binmode + tmpFile.print img.to_blob + tmpFile + ensure + tmpFile.close + end + + # + # Extract info from a PNG file + # @access protected + # + def parsepng(file) + f=open(file,'rb'); + #Check signature + if (f.read(8)!=137.chr + 'PNG' + 13.chr + 10.chr + 26.chr + 10.chr) + Error('Not a PNG file: ' + file); + end + #Read header chunk + f.read(4); + if (f.read(4)!='IHDR') + Error('Incorrect PNG file: ' + file); + end + w=freadint(f); + h=freadint(f); + bpc=f.read(1).unpack('C')[0]; + if (bpc>8) + Error('16-bit depth not supported: ' + file); + end + ct=f.read(1).unpack('C')[0]; + if (ct==0) + colspace='DeviceGray'; + elsif (ct==2) + colspace='DeviceRGB'; + elsif (ct==3) + colspace='Indexed'; + else + Error('Alpha channel not supported: ' + file); + end + if (f.read(1).unpack('C')[0] != 0) + Error('Unknown compression method: ' + file); + end + if (f.read(1).unpack('C')[0] != 0) + Error('Unknown filter method: ' + file); + end + if (f.read(1).unpack('C')[0] != 0) + Error('Interlacing not supported: ' + file); + end + f.read(4); + parms='/DecodeParms <>'; + #Scan chunks looking for palette, transparency and image data + pal=''; + trns=''; + data=''; + begin + n=freadint(f); + type=f.read(4); + if (type=='PLTE') + #Read palette + pal=f.read( n); + f.read(4); + elsif (type=='tRNS') + #Read transparency info + t=f.read( n); + if (ct==0) + trns = t[1].unpack('C')[0] + elsif (ct==2) + trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]] + else + pos=t.index(0.chr); + unless (pos.nil?) + trns = [pos] + end + end + f.read(4); + elsif (type=='IDAT') + #Read image data block + data< w, 'h' => h, 'cs' => colspace, 'bpc' => bpc, 'f'=>'FlateDecode', 'parms' => parms, 'pal' => pal, 'trns' => trns, 'data' => data} + ensure + f.close + end + + # + # Read a 4-byte integer from file + # @access protected + # + def freadint(f) + # Read a 4-byte integer from file + a = f.read(4).unpack('N') + return a[0] + end + + # + # Format a text string + # @access protected + # + def textstring(s) + if (@is_unicode) + #Convert string to UTF-16BE + s = UTF8ToUTF16BE(s, true); + end + return '(' + escape(s) + ')'; + end + + # + # Format a text string + # @access protected + # + def escapetext(s) + if (@is_unicode) + #Convert string to UTF-16BE + s = UTF8ToUTF16BE(s, false); + end + return escape(s); + end + + # + # Add \ before \, ( and ) + # @access protected + # + def escape(s) + # Add \ before \, ( and ) + s.gsub('\\','\\\\\\').gsub('(','\\(').gsub(')','\\)').gsub(13.chr, '\r') + end + + # + # + # @access protected + # + def putstream(s) + out('stream'); + out(s); + out('endstream'); + end + + # + # Add a line to the document + # @access protected + # + def out(s) + if (@state==2) + @pages[@page] << s.to_s + "\n"; + else + @buffer << s.to_s + "\n"; + end + end + + # + # Adds unicode fonts.
+ # Based on PDF Reference 1.3 (section 5) + # @access protected + # @author Nicola Asuni + # @since 1.52.0.TC005 (2005-01-05) + # + def puttruetypeunicode(font) + # Type0 Font + # A composite font composed of other fonts, organized hierarchically + newobj(); + out('<>'); + out('endobj'); + + # CIDFontType2 + # A CIDFont whose glyph descriptions are based on TrueType font technology + newobj(); + out('<>'); + out('endobj'); + + # ToUnicode + # is a stream object that contains the definition of the CMap + # (PDF Reference 1.3 chap. 5.9) + newobj(); + out('<>') + out('stream'); + out('/CIDInit /ProcSet findresource begin'); + out('12 dict begin'); + out('begincmap'); + out('/CIDSystemInfo'); + out('<> def'); + out('/CMapName /Adobe-Identity-UCS def'); + out('/CMapType 2 def'); + out('1 begincodespacerange'); + out('<0000> '); + out('endcodespacerange'); + out('1 beginbfrange'); + out('<0000> <0000>'); + out('endbfrange'); + out('endcmap'); + out('CMapName currentdict /CMap defineresource pop'); + out('end'); + out('end'); + out('endstream'); + out('endobj'); + + # CIDSystemInfo dictionary + # A dictionary containing entries that define the character collection of the CIDFont. + newobj(); + out('<>'); + out('endobj'); + + # Font descriptor + # A font descriptor describing the CIDFont default metrics other than its glyph widths + newobj(); + out('<>'); + out('endobj'); + + # Embed CIDToGIDMap + # A specification of the mapping from CIDs to glyph indices + newobj(); + ctgfile = getfontpath(font['ctg']) + if (!ctgfile) + Error('Font file not found: ' + ctgfile); + end + size = File.size(ctgfile); + out('<>'); + open(ctgfile, "rb") do |f| + putstream(f.read()) + end + out('endobj'); + end + + # + # Converts UTF-8 strings to codepoints array.
+ # Invalid byte sequences will be replaced with 0xFFFD (replacement character)
+ # Based on: http://www.faqs.org/rfcs/rfc3629.html + #
+	# 	  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.
+ # Based on: http://www.faqs.org/rfcs/rfc2781.html + #
+	#   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.
+ # Supports: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, ins, del, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small + # @param string :html text to display + # @param boolean :ln if true add a new line after text (default = true) + # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. + # + def writeHTML(html, ln=true, fill=0, h=0) + + @lasth = h if h > 0 + if (@lasth == 0) + #set row height + @lasth = @font_size * @@k_cell_height_ratio; + end + + @href = nil + @style = ""; + @t_cells = [[]]; + @table_id = 0; + + # pre calculate + html.split(/(<[^>]+>)/).each do |element| + if "<" == element[0,1] + #Tag + if (element[1, 1] == '/') + closedHTMLTagCalc(element[2..-2].downcase); + else + #Extract attributes + # get tag name + tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} + tag = tag[0].to_s.downcase; + + # get attributes + attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) + attrs = {} + attr_array.each do |name, value| + attrs[name.downcase] = value; + end + openHTMLTagCalc(tag, attrs); + end + end + end + @table_id = 0; + + html.split(/(<[A-Za-z!?\/][^>]*?>)/).each do |element| + if "<" == element[0,1] + #Tag + if (element[1, 1] == '/') + closedHTMLTagHandler(element[2..-2].downcase); + else + #Extract attributes + # get tag name + tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} + tag = tag[0].to_s.downcase; + + # get attributes + attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) + attrs = {} + attr_array.each do |name, value| + attrs[name.downcase] = value; + end + openHTMLTagHandler(tag, attrs, fill); + end + + else + #Text + if (@tdbegin) + element.gsub!(/[\t\r\n\f]/, ""); + @tdtext << element.gsub(/ /, " "); + elsif (@href) + element.gsub!(/[\t\r\n\f]/, ""); + addHtmlLink(@href, element, fill); + elsif (@pre_state == true and element.length > 0) + Write(@lasth, unhtmlentities(element), '', fill); + elsif (element.strip.length > 0) + element.gsub!(/[\t\r\n\f]/, ""); + element.gsub!(/ /, " "); + Write(@lasth, unhtmlentities(element), '', fill); + end + end + end + + if (ln) + Ln(@lasth); + end + end + alias_method :write_html, :writeHTML + + # + # Prints a cell (rectangular area) with optional borders, background color and html text string. The upper-left corner of the cell corresponds to the current position. After the call, the current position moves to the right or to the next line.
+ # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. + # @param float :w Cell width. If 0, the cell extends up to the right margin. + # @param float :h Cell minimum height. The cell extends automatically if needed. + # @param float :x upper-left corner X coordinate + # @param float :y upper-left corner Y coordinate + # @param string :html html text to print. Default value: empty string. + # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
  • 0: no border (default)
  • 1: frame
or a string containing some or all of the following characters (in any order):
  • L: left
  • T: top
  • R: right
  • B: bottom
+ # @param int :ln Indicates where the current position should go after the call. Possible values are:
  • 0: to the right
  • 1: to the beginning of the next line
  • 2: below
+# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @see Cell() + # + def writeHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0) + + if (@lasth == 0) + #set row height + @lasth = @font_size * @@k_cell_height_ratio; + end + + if (x == 0) + x = GetX(); + end + if (y == 0) + y = GetY(); + end + + # get current page number + pagenum = @page; + + SetX(x); + SetY(y); + + if (w == 0) + w = @fw - x - @r_margin; + end + + b=0; + if (border) + if (border==1) + border='LTRB'; + b='LRT'; + b2='LR'; + elsif border.is_a?(String) + b2=''; + if (border.include?('L')) + b2<<'L'; + end + if (border.include?('R')) + b2<<'R'; + end + b=(border.include?('T')) ? b2 + 'T' : b2; + end + end + + # store original margin values + l_margin = @l_margin; + r_margin = @r_margin; + + # set new margin values + SetLeftMargin(x); + SetRightMargin(@fw - x - w); + + # calculate remaining vertical space on page + restspace = GetPageHeight() - GetY() - GetBreakMargin(); + + writeHTML(html, true, fill); # write html text + SetX(x) + + currentY = GetY(); + @auto_page_break = false; + # check if a new page has been created + if (@page > pagenum) + # design a cell around the text on first page + currentpage = @page; + @page = pagenum; + SetY(GetPageHeight() - restspace - GetBreakMargin()); + SetX(x) + Cell(w, restspace - 1, "", b, 0, 'L', 0); + b = b2; + @page += 1; + while @page < currentpage + SetY(@t_margin); # put cursor at the beginning of text + SetX(x) + Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0); + @page += 1; + end + if (border.is_a?(String) and border.include?('B')) + b<<'B'; + end + # design a cell around the text on last page + SetY(@t_margin); # put cursor at the beginning of text + SetX(x) + Cell(w, currentY - @t_margin, "", b, 0, 'L', 0); + else + SetY(y); # put cursor at the beginning of text + # design a cell around the text + SetX(x) + Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0); + end + @auto_page_break = true; + + # restore original margin values + SetLeftMargin(l_margin); + SetRightMargin(r_margin); + + @lasth = h + + # move cursor to specified position + if (ln == 0) + # go to the top-right of the cell + @x = x + w; + @y = y; + elsif (ln == 1) + # go to the beginning of the next line + @x = @l_margin; + @y = currentY; + elsif (ln == 2) + # go to the bottom-left of the cell (below) + @x = x; + @y = currentY; + end + end + alias_method :write_html_cell, :writeHTMLCell + + # + # Check html table tag position. + # + # @param array :table potision array + # @param int :current tr tag id number + # @param int :current td tag id number + # @access private + # @return int : next td_id position. + # value 0 mean that can use position. + # + def checkTableBlockingCellPosition(table, tr_id, td_id ) + 0.upto(tr_id) do |j| + 0.upto(@t_cells[table][j].size - 1) do |i| + if @t_cells[table][j][i]['i0'] <= td_id and td_id <= @t_cells[table][j][i]['i1'] + if @t_cells[table][j][i]['j0'] <= tr_id and tr_id <= @t_cells[table][j][i]['j1'] + return @t_cells[table][j][i]['i1'] - td_id + 1; + end + end + end + end + return 0; + end + + # + # Calculate opening tags. + # + # html table cell array : @t_cells + # + # i0: table cell start position + # i1: table cell end position + # j0: table row start position + # j1: table row end position + # + # +------+ + # |i0,j0 | + # | i1,j1| + # +------+ + # + # example html: + # + # + # + # + # + #
+ # + # i: 0 1 2 + # j+----+----+----+ + # :|0,0 |1,0 |2,0 | + # 0| 0,0| 1,0| 2,0| + # +----+----+----+ + # |0,1 |2,1 | + # 1| 1,1| 2,1| + # +----+----+----+ + # |0,2 |1,2 |2,2 | + # 2| | 1,2| 2,2| + # + +----+----+ + # | |1,3 |2,3 | + # 3| 0,3| 1,3| 2,3| + # +----+----+----+ + # + # html table cell array : + # [[[i0=>0,j0=>0,i1=>0,j1=>0],[i0=>1,j0=>0,i1=>1,j1=>0],[i0=>2,j0=>0,i1=>2,j1=>0]], + # [[i0=>0,j0=>1,i1=>1,j1=>1],[i0=>2,j0=>1,i1=>2,j1=>1]], + # [[i0=>0,j0=>2,i1=>0,j1=>3],[i0=>1,j0=>2,i1=>1,j1=>2],[i0=>2,j0=>2,i1=>2,j1=>2]] + # [[i0=>1,j0=>3,i1=>1,j1=>3],[i0=>2,j0=>3,i1=>2,j1=>3]]] + # + # @param string :tag tag name (in upcase) + # @param string :attr tag attribute (in upcase) + # @access private + # + def openHTMLTagCalc(tag, attrs) + #Opening tag + case (tag) + when 'table' + @max_table_columns[@table_id] = 0; + @t_columns = 0; + @tr_id = -1; + when 'tr' + if @max_table_columns[@table_id] < @t_columns + @max_table_columns[@table_id] = @t_columns; + end + @t_columns = 0; + @tr_id += 1; + @td_id = -1; + @t_cells[@table_id].push [] + when 'td', 'th' + @td_id += 1; + if attrs['colspan'].nil? or attrs['colspan'] == '' + colspan = 1; + else + colspan = attrs['colspan'].to_i; + end + if attrs['rowspan'].nil? or attrs['rowspan'] == '' + rowspan = 1; + else + rowspan = attrs['rowspan'].to_i; + end + + i = 0; + while true + next_i_distance = checkTableBlockingCellPosition(@table_id, @tr_id, @td_id + i); + if next_i_distance == 0 + @t_cells[@table_id][@tr_id].push "i0"=>@td_id + i, "j0"=>@tr_id, "i1"=>(@td_id + i + colspan - 1), "j1"=>@tr_id + rowspan - 1 + break; + end + i += next_i_distance; + end + + @t_columns += colspan; + end + end + + # + # Calculate closing tags. + # @param string :tag tag name (in upcase) + # @access private + # + def closedHTMLTagCalc(tag) + #Closing tag + case (tag) + when 'table' + if @max_table_columns[@table_id] < @t_columns + @max_table_columns[@table_id] = @t_columns; + end + @table_id += 1; + @t_cells.push [] + end + end + + # + # Convert to accessible file path + # @param string :attrname image file name + # + def getImageFilename( attrname ) + nil + end + + # + # Process opening tags. + # @param string :tag tag name (in upcase) + # @param string :attr tag attribute (in upcase) + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @access private + # + def openHTMLTagHandler(tag, attrs, fill=0) + #Opening tag + case (tag) + when 'pre' + @pre_state = true; + @l_margin += 5; + @r_margin += 5; + @x += 5; + + when 'table' + Ln(); + if @default_table_columns < @max_table_columns[@table_id] + @table_columns = @max_table_columns[@table_id]; + else + @table_columns = @default_table_columns; + end + @l_margin += 5; + @r_margin += 5; + @x += 5; + + if attrs['border'].nil? or attrs['border'] == '' + @tableborder = 0; + else + @tableborder = attrs['border']; + end + @tr_id = -1; + @max_td_page[0] = @page; + @max_td_y[0] = @y; + + when 'tr', 'td', 'th' + if tag == 'th' + SetStyle('b', true); + @tdalign = "C"; + end + if ((!attrs['width'].nil?) and (attrs['width'] != '')) + @tdwidth = (attrs['width'].to_i/4); + else + @tdwidth = ((@w - @l_margin - @r_margin) / @table_columns); + end + + if tag == 'tr' + @tr_id += 1; + @td_id = -1; + else + @td_id += 1; + @x = @l_margin + @tdwidth * @t_cells[@table_id][@tr_id][@td_id]['i0']; + end + + if attrs['colspan'].nil? or attrs['border'] == '' + @colspan = 1; + else + @colspan = attrs['colspan'].to_i; + end + @tdwidth *= @colspan; + if ((!attrs['height'].nil?) and (attrs['height'] != '')) + @tdheight=(attrs['height'].to_i / @k); + else + @tdheight = @lasth; + end + if ((!attrs['align'].nil?) and (attrs['align'] != '')) + case (attrs['align']) + when 'center' + @tdalign = "C"; + when 'right' + @tdalign = "R"; + when 'left' + @tdalign = "L"; + end + end + if ((!attrs['bgcolor'].nil?) and (attrs['bgcolor'] != '')) + coul = convertColorHexToDec(attrs['bgcolor']); + SetFillColor(coul['R'], coul['G'], coul['B']); + @tdfill=1; + end + @tdbegin=true; + + when 'hr' + margin = 1; + if ((!attrs['width'].nil?) and (attrs['width'] != '')) + hrWidth = attrs['width']; + else + hrWidth = @w - @l_margin - @r_margin - margin; + end + SetLineWidth(0.2); + Line(@x + margin, @y, @x + hrWidth, @y); + Ln(); + + when 'strong' + SetStyle('b', true); + + when 'em' + SetStyle('i', true); + + when 'ins' + SetStyle('u', true); + + when 'del' + SetStyle('d', true); + + when 'b', 'i', 'u' + SetStyle(tag, true); + + when 'a' + @href = attrs['href']; + + when 'img' + if (!attrs['src'].nil?) + # Don't generates image inside table tag + if (@tdbegin) + @tdtext << attrs['src']; + return + end + # Only generates image include a pdf if RMagick is avalaible + unless Object.const_defined?(:Magick) + Write(@lasth, attrs['src'], '', fill); + return + end + file = getImageFilename(attrs['src']) + if (file.nil?) + Write(@lasth, attrs['src'], '', fill); + return + end + + if (attrs['width'].nil?) + attrs['width'] = 0; + end + if (attrs['height'].nil?) + attrs['height'] = 0; + end + + begin + Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height'])); + #SetX(@img_rb_x); + SetY(@img_rb_y); + rescue => err + logger.error "pdf: Image: error: #{err.message}" + Write(@lasth, attrs['src'], '', fill); + end + end + + when 'ul', 'ol' + if @li_count == 0 + Ln() if @prevquote_count == @quote_count; # insert Ln for keeping quote lines + @prevquote_count = @quote_count; + end + if @li_state == true + Ln(); + @li_state = false; + end + if tag == 'ul' + @list_ordered[@li_count] = false; + else + @list_ordered[@li_count] = true; + end + @list_count[@li_count] = 0; + @li_count += 1 + + when 'li' + Ln() if @li_state == true + if (@list_ordered[@li_count - 1]) + @list_count[@li_count - 1] += 1; + @li_spacer = " " * @li_count + (@list_count[@li_count - 1]).to_s + ". "; + else + #unordered list simbol + @li_spacer = " " * @li_count + "- "; + end + Write(@lasth, @spacer + @li_spacer, '', fill); + @li_state = true; + + when 'blockquote' + if (@quote_count == 0) + SetStyle('i', true); + @l_margin += 5; + else + @l_margin += 5 / 2; + end + @x = @l_margin; + @quote_top[@quote_count] = @y; + @quote_page[@quote_count] = @page; + @quote_count += 1 + when 'br' + if @tdbegin + @tdtext << "\n" + return + end + Ln(); + + if (@li_spacer.length > 0) + @x += GetStringWidth(@li_spacer); + end + + when 'p' + Ln(); + 0.upto(@quote_count - 1) do |i| + if @quote_page[i] == @page; + if @quote_top[i] == @y - @lasth; # fix start line + @quote_top[i] = @y; + end + else + if @quote_page[i] == @page - 1; + @quote_page[i] = @page; # fix start line + @quote_top[i] = @t_margin; + end + end + end + + when 'sup' + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt * @@k_small_ratio); + SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'sub' + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt * @@k_small_ratio); + SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'small' + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt * @@k_small_ratio); + SetXY(GetX(), GetY() + ((currentfont_size - @font_size)/3)); + + when 'font' + if (!attrs['color'].nil? and attrs['color']!='') + coul = convertColorHexToDec(attrs['color']); + SetTextColor(coul['R'], coul['G'], coul['B']); + @issetcolor=true; + end + if (!attrs['face'].nil? and @fontlist.include?(attrs['face'].downcase)) + SetFont(attrs['face'].downcase); + @issetfont=true; + end + if (!attrs['size'].nil?) + headsize = attrs['size'].to_i; + else + headsize = 0; + end + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt + headsize); + @lasth = @font_size * @@k_cell_height_ratio; + + when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' + Ln(); + headsize = (4 - tag[1,1].to_f) * 2 + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt + headsize); + SetStyle('b', true); + @lasth = @font_size * @@k_cell_height_ratio; + + end + end + + # + # Process closing tags. + # @param string :tag tag name (in upcase) + # @access private + # + def closedHTMLTagHandler(tag) + #Closing tag + case (tag) + when 'pre' + @pre_state = false; + @l_margin -= 5; + @r_margin -= 5; + @x = @l_margin; + Ln(); + + when 'td','th' + base_page = @page; + base_x = @x; + base_y = @y; + + MultiCell(@tdwidth, @tdheight, unhtmlentities(@tdtext.strip), @tableborder, @tdalign, @tdfill, 1); + tr_end = @t_cells[@table_id][@tr_id][@td_id]['j1'] + 1; + if @max_td_page[tr_end].nil? or (@max_td_page[tr_end] < @page) + @max_td_page[tr_end] = @page + @max_td_y[tr_end] = @y + elsif (@max_td_page[tr_end] == @page) + @max_td_y[tr_end] = @y if @max_td_y[tr_end].nil? or (@max_td_y[tr_end] < @y) + end + + @page = base_page; + @x = base_x + @tdwidth; + @y = base_y; + @tdtext = ''; + @tdbegin = false; + @tdwidth = 0; + @tdheight = 0; + @tdalign = "L"; + SetStyle('b', false); + @tdfill = 0; + SetFillColor(@prevfill_color[0], @prevfill_color[1], @prevfill_color[2]); + + when 'tr' + @y = @max_td_y[@tr_id + 1]; + @x = @l_margin; + @page = @max_td_page[@tr_id + 1]; + + when 'table' + # Write Table Line + width = (@w - @l_margin - @r_margin) / @table_columns; + 0.upto(@t_cells[@table_id].size - 1) do |j| + 0.upto(@t_cells[@table_id][j].size - 1) do |i| + @page = @max_td_page[j] + i0=@t_cells[@table_id][j][i]['i0']; + j0=@t_cells[@table_id][j][i]['j0']; + i1=@t_cells[@table_id][j][i]['i1']; + j1=@t_cells[@table_id][j][i]['j1']; + + Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j0]) # top + if ( @page == @max_td_page[j1 + 1]) + Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @max_td_y[j1+1]) # left + Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j1+1]) # right + else + Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @page_break_trigger) # left + Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @page_break_trigger) # right + @page += 1; + while @page < @max_td_page[j1 + 1] + Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @page_break_trigger) # left + Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @page_break_trigger) # right + @page += 1; + end + Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @max_td_y[j1+1]) # left + Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @max_td_y[j1+1]) # right + end + Line(@l_margin + width * i0, @max_td_y[j1+1], @l_margin + width * (i1+1), @max_td_y[j1+1]) # bottom + end + end + + @l_margin -= 5; + @r_margin -= 5; + @tableborder=0; + @table_id += 1; + + when 'strong' + SetStyle('b', false); + + when 'em' + SetStyle('i', false); + + when 'ins' + SetStyle('u', false); + + when 'del' + SetStyle('d', false); + + when 'b', 'i', 'u' + SetStyle(tag, false); + + when 'a' + @href = nil; + + when 'p' + Ln(); + + when 'sup' + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'sub' + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'small' + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetXY(GetX(), GetY() - ((@font_size - currentfont_size)/3)); + + when 'font' + if (@issetcolor == true) + SetTextColor(@prevtext_color[0], @prevtext_color[1], @prevtext_color[2]); + end + if (@issetfont) + @font_family = @prevfont_family; + @font_style = @prevfont_style; + SetFont(@font_family); + @issetfont = false; + end + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + #@text_color = @prevtext_color; + @lasth = @font_size * @@k_cell_height_ratio; + + when 'blockquote' + @quote_count -= 1 + if (@quote_page[@quote_count] == @page) + Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @y) # quoto line + else + cur_page = @page; + cur_y = @y; + @page = @quote_page[@quote_count]; + if (@quote_top[@quote_count] < @page_break_trigger) + Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @page_break_trigger) # quoto line + end + @page += 1; + while @page < cur_page + Line(@l_margin - 1, @t_margin, @l_margin - 1, @page_break_trigger) # quoto line + @page += 1; + end + @y = cur_y; + Line(@l_margin - 1, @t_margin, @l_margin - 1, @y) # quoto line + end + if (@quote_count <= 0) + SetStyle('i', false); + @l_margin -= 5; + else + @l_margin -= 5 / 2; + end + @x = @l_margin; + Ln() if @quote_count == 0 + + when 'ul', 'ol' + @li_count -= 1 + if @li_state == true + Ln(); + @li_state = false; + end + + when 'li' + @li_spacer = ""; + if @li_state == true + Ln(); + @li_state = false; + end + + when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetStyle('b', false); + Ln(); + @lasth = @font_size * @@k_cell_height_ratio; + + if tag == 'h1' or tag == 'h2' or tag == 'h3' or tag == 'h4' + margin = 1; + hrWidth = @w - @l_margin - @r_margin - margin; + if tag == 'h1' or tag == 'h2' + SetLineWidth(0.2); + else + SetLineWidth(0.1); + end + Line(@x + margin, @y, @x + hrWidth, @y); + end + end + end + + # + # Sets font style. + # @param string :tag tag name (in lowercase) + # @param boolean :enable + # @access private + # + def SetStyle(tag, enable) + #Modify style and select corresponding font + ['b', 'i', 'u', 'd'].each do |s| + if tag.downcase == s + if enable + @style << s if ! @style.include?(s) + else + @style = @style.gsub(s,'') + end + end + end + SetFont('', @style); + end + + # + # Output anchor link. + # @param string :url link URL + # @param string :name link name + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @access public + # + def addHtmlLink(url, name, fill=0) + #Put a hyperlink + SetTextColor(0, 0, 255); + SetStyle('u', true); + Write(@lasth, name, url, fill); + SetStyle('u', false); + SetTextColor(0); + end + + # + # Returns an associative array (keys: R,G,B) from + # a hex html code (e.g. #3FE5AA). + # @param string :color hexadecimal html color [#rrggbb] + # @return array + # @access private + # + def convertColorHexToDec(color = "#000000") + tbl_color = {} + tbl_color['R'] = color[1,2].hex.to_i; + tbl_color['G'] = color[3,2].hex.to_i; + tbl_color['B'] = color[5,2].hex.to_i; + return tbl_color; + end + + # + # Converts pixels to millimeters in 72 dpi. + # @param int :px pixels + # @return float millimeters + # @access private + # + def pixelsToMillimeters(px) + return px.to_f * 25.4 / 72; + end + + # + # Reverse function for htmlentities. + # Convert entities in UTF-8. + # + # @param :text_to_convert Text to convert. + # @return string converted + # + def unhtmlentities(string) + CGI.unescapeHTML(string) + end + +end # END OF CLASS + +#TODO 2007-05-25 (EJM) Level=0 - +#Handle special IE contype request +# if (!_SERVER['HTTP_USER_AGENT'].nil? and (_SERVER['HTTP_USER_AGENT']=='contype')) +# header('Content-Type: application/pdf'); +# exit; +# } diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/002350bfb6f2834394af5b5afbf87a58fa102afc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/002350bfb6f2834394af5b5afbf87a58fa102afc.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1116 @@ +#Ernad Husremovic hernad@bring.out.ba + +bs: + direction: ltr + date: + formats: + default: "%d.%m.%Y" + short: "%e. %b" + long: "%e. %B %Y" + only_day: "%e" + + + day_names: [Nedjelja, Ponedjeljak, Utorak, Srijeda, ÄŒetvrtak, Petak, Subota] + abbr_day_names: [Ned, Pon, Uto, Sri, ÄŒet, Pet, Sub] + + month_names: [~, Januar, Februar, Mart, April, Maj, Jun, Jul, Avgust, Septembar, Oktobar, Novembar, Decembar] + abbr_month_names: [~, Jan, Feb, Mar, Apr, Maj, Jun, Jul, Avg, Sep, Okt, Nov, Dec] + order: + - :day + - :month + - :year + + time: + formats: + default: "%A, %e. %B %Y, %H:%M" + short: "%e. %B, %H:%M Uhr" + long: "%A, %e. %B %Y, %H:%M" + time: "%H:%M" + + am: "prijepodne" + pm: "poslijepodne" + + datetime: + distance_in_words: + half_a_minute: "pola minute" + less_than_x_seconds: + one: "manje od 1 sekunde" + other: "manje od %{count} sekudni" + x_seconds: + one: "1 sekunda" + other: "%{count} sekundi" + less_than_x_minutes: + one: "manje od 1 minute" + other: "manje od %{count} minuta" + x_minutes: + one: "1 minuta" + other: "%{count} minuta" + about_x_hours: + one: "oko 1 sahat" + other: "oko %{count} sahata" + x_hours: + one: "1 sahat" + other: "%{count} sahata" + x_days: + one: "1 dan" + other: "%{count} dana" + about_x_months: + one: "oko 1 mjesec" + other: "oko %{count} mjeseci" + x_months: + one: "1 mjesec" + other: "%{count} mjeseci" + about_x_years: + one: "oko 1 godine" + other: "oko %{count} godina" + over_x_years: + one: "preko 1 godine" + other: "preko %{count} godina" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + + number: + format: + precision: 2 + separator: ',' + delimiter: '.' + currency: + format: + unit: 'KM' + format: '%u %n' + negative_format: '%u -%n' + delimiter: '' + percentage: + format: + delimiter: "" + precision: + format: + delimiter: "" + 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: "i" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "nije ukljuÄeno u listu" + exclusion: "je rezervisano" + invalid: "nije ispravno" + confirmation: "ne odgovara potvrdi" + accepted: "mora se prihvatiti" + empty: "ne može biti prazno" + blank: "ne može biti znak razmaka" + too_long: "je predugaÄko" + too_short: "je prekratko" + wrong_length: "je pogreÅ¡ne dužine" + taken: "već je zauzeto" + not_a_number: "nije broj" + not_a_date: "nije ispravan datum" + greater_than: "mora bit veći od %{count}" + greater_than_or_equal_to: "mora bit veći ili jednak %{count}" + equal_to: "mora biti jednak %{count}" + less_than: "mora biti manji od %{count}" + less_than_or_equal_to: "mora bit manji ili jednak %{count}" + odd: "mora biti neparan" + even: "mora biti paran" + greater_than_start_date: "mora biti veći nego poÄetni datum" + not_same_project: "ne pripada istom projektu" + circular_dependency: "Ova relacija stvar cirkularnu zavisnost" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Molimo odaberite + + general_text_No: 'Da' + general_text_Yes: 'Ne' + general_text_no: 'ne' + general_text_yes: 'da' + general_lang_name: 'Bosanski' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_activated: VaÅ¡ nalog je aktiviran. Možete se prijaviti. + notice_account_invalid_creditentials: PogreÅ¡an korisnik ili lozinka + notice_account_lost_email_sent: Email sa uputstvima o izboru nove Å¡ifre je poslat na vaÅ¡u adresu. + notice_account_password_updated: Lozinka je uspjeÅ¡no promjenjena. + notice_account_pending: "VaÅ¡ nalog je kreiran i Äeka odobrenje administratora." + notice_account_register_done: Nalog je uspjeÅ¡no kreiran. Da bi ste aktivirali vaÅ¡ nalog kliknite na link koji vam je poslat. + notice_account_unknown_email: Nepoznati korisnik. + notice_account_updated: Nalog je uspjeÅ¡no promjenen. + notice_account_wrong_password: PogreÅ¡na lozinka + notice_can_t_change_password: Ovaj nalog koristi eksterni izvor prijavljivanja. Ne mogu da promjenim Å¡ifru. + notice_default_data_loaded: Podrazumjevana konfiguracija uspjeÄno uÄitana. + notice_email_error: DoÅ¡lo je do greÅ¡ke pri slanju emaila (%{value}) + notice_email_sent: "Email je poslan %{value}" + notice_failed_to_save_issues: "NeuspjeÅ¡no snimanje %{count} aktivnosti na %{total} izabrano: %{ids}." + notice_feeds_access_key_reseted: VaÅ¡ Atom pristup je resetovan. + notice_file_not_found: Stranica kojoj pokuÅ¡avate da pristupite ne postoji ili je uklonjena. + notice_locking_conflict: "Konflikt: podaci su izmjenjeni od strane drugog korisnika." + notice_no_issue_selected: "Nijedna aktivnost nije izabrana! Molim, izaberite aktivnosti koje želite za ispravljate." + notice_not_authorized: Niste ovlašćeni da pristupite ovoj stranici. + notice_successful_connection: UspjeÅ¡na konekcija. + notice_successful_create: UspjeÅ¡no kreiranje. + notice_successful_delete: Brisanje izvrÅ¡eno. + notice_successful_update: Promjene uspjeÅ¡no izvrÅ¡ene. + + error_can_t_load_default_data: "Podrazumjevane postavke se ne mogu uÄitati %{value}" + error_scm_command_failed: "Desila se greÅ¡ka pri pristupu repozitoriju: %{value}" + error_scm_not_found: "Unos i/ili revizija ne postoji u repozitoriju." + + error_scm_annotate: "Ova stavka ne postoji ili nije oznaÄena." + error_issue_not_found_in_project: 'Aktivnost nije naÄ‘ena ili ne pripada ovom projektu' + + warning_attachments_not_saved: "%{count} fajl(ovi) ne mogu biti snimljen(i)." + + mail_subject_lost_password: "VaÅ¡a %{value} lozinka" + mail_body_lost_password: 'Za promjenu lozinke, kliknite na sljedeći link:' + mail_subject_register: "Aktivirajte %{value} vaÅ¡ korisniÄki raÄun" + mail_body_register: 'Za aktivaciju vaÅ¡eg korisniÄkog raÄuna, kliknite na sljedeći link:' + mail_body_account_information_external: "Možete koristiti vaÅ¡ %{value} korisniÄki raÄun za prijavu na sistem." + mail_body_account_information: Informacija o vaÅ¡em korisniÄkom raÄunu + mail_subject_account_activation_request: "%{value} zahtjev za aktivaciju korisniÄkog raÄuna" + mail_body_account_activation_request: "Novi korisnik (%{value}) se registrovao. KorisniÄki raÄun Äeka vaÅ¡e odobrenje za aktivaciju:" + mail_subject_reminder: "%{count} aktivnost(i) u kaÅ¡njenju u narednim %{days} danima" + mail_body_reminder: "%{count} aktivnost(i) koje su dodjeljenje vama u narednim %{days} danima:" + + + field_name: Ime + field_description: Opis + field_summary: PojaÅ¡njenje + field_is_required: Neophodno popuniti + field_firstname: Ime + field_lastname: Prezime + field_mail: Email + field_filename: Fajl + field_filesize: VeliÄina + field_downloads: Downloadi + field_author: Autor + field_created_on: Kreirano + field_updated_on: Izmjenjeno + field_field_format: Format + field_is_for_all: Za sve projekte + field_possible_values: Moguće vrijednosti + field_regexp: '"Regularni izraz"' + field_min_length: Minimalna veliÄina + field_max_length: Maksimalna veliÄina + field_value: Vrijednost + field_category: Kategorija + field_title: Naslov + field_project: Projekat + field_issue: Aktivnost + field_status: Status + field_notes: BiljeÅ¡ke + field_is_closed: Aktivnost zatvorena + field_is_default: Podrazumjevana vrijednost + field_tracker: PodruÄje aktivnosti + field_subject: Subjekat + field_due_date: ZavrÅ¡iti do + field_assigned_to: Dodijeljeno + field_priority: Prioritet + field_fixed_version: Ciljna verzija + field_user: Korisnik + field_role: Uloga + field_homepage: Naslovna strana + field_is_public: Javni + field_parent: Podprojekt od + field_is_in_roadmap: Aktivnosti prikazane u planu realizacije + field_login: Prijava + field_mail_notification: Email notifikacije + field_admin: Administrator + field_last_login_on: Posljednja konekcija + field_language: Jezik + field_effective_date: Datum + field_password: Lozinka + field_new_password: Nova lozinka + field_password_confirmation: Potvrda + field_version: Verzija + field_type: Tip + field_host: Host + field_port: Port + field_account: KorisniÄki raÄun + field_base_dn: Base DN + field_attr_login: Attribut za prijavu + field_attr_firstname: Attribut za ime + field_attr_lastname: Atribut za prezime + field_attr_mail: Atribut za email + field_onthefly: 'Kreiranje korisnika "On-the-fly"' + field_start_date: PoÄetak + field_done_ratio: "% Realizovano" + field_auth_source: Mod za authentifikaciju + field_hide_mail: Sakrij moju email adresu + field_comments: Komentar + field_url: URL + field_start_page: PoÄetna stranica + field_subproject: Podprojekat + field_hours: Sahata + field_activity: Operacija + field_spent_on: Datum + field_identifier: Identifikator + field_is_filter: KoriÅ¡teno kao filter + field_issue_to: Povezana aktivnost + field_delay: OdgaÄ‘anje + field_assignable: Aktivnosti dodijeljene ovoj ulozi + field_redirect_existing_links: IzvrÅ¡i redirekciju postojećih linkova + field_estimated_hours: Procjena vremena + field_column_names: Kolone + field_time_zone: Vremenska zona + field_searchable: Pretraživo + field_default_value: Podrazumjevana vrijednost + field_comments_sorting: Prikaži komentare + field_parent_title: 'Stranica "roditelj"' + field_editable: Može se mijenjati + field_watcher: PosmatraÄ + field_identity_url: OpenID URL + field_content: Sadržaj + + setting_app_title: Naslov aplikacije + setting_app_subtitle: Podnaslov aplikacije + setting_welcome_text: Tekst dobrodoÅ¡lice + setting_default_language: Podrazumjevani jezik + setting_login_required: Authentifikacija neophodna + setting_self_registration: Samo-registracija + setting_attachment_max_size: Maksimalna veliÄina prikaÄenog fajla + setting_issues_export_limit: Limit za eksport aktivnosti + setting_mail_from: Mail adresa - poÅ¡aljilac + setting_bcc_recipients: '"BCC" (blind carbon copy) primaoci ' + setting_plain_text_mail: Email sa obiÄnim tekstom (bez HTML-a) + setting_host_name: Ime hosta i putanja + setting_text_formatting: Formatiranje teksta + setting_wiki_compression: Kompresija Wiki istorije + + setting_feeds_limit: 'Limit za "Atom" feed-ove' + setting_default_projects_public: Podrazumjeva se da je novi projekat javni + setting_autofetch_changesets: 'Automatski kupi "commit"-e' + setting_sys_api_enabled: 'Omogući "WS" za upravljanje repozitorijom' + setting_commit_ref_keywords: KljuÄne rijeÄi za reference + setting_commit_fix_keywords: 'KljuÄne rijeÄi za status "zatvoreno"' + setting_autologin: Automatski login + setting_date_format: Format datuma + setting_time_format: Format vremena + setting_cross_project_issue_relations: Omogući relacije izmeÄ‘u aktivnosti na razliÄitim projektima + setting_issue_list_default_columns: Podrazumjevane koleone za prikaz na listi aktivnosti + setting_emails_footer: Potpis na email-ovima + setting_protocol: Protokol + setting_per_page_options: Broj objekata po stranici + setting_user_format: Format korisniÄkog prikaza + setting_activity_days_default: Prikaz promjena na projektu - opseg dana + setting_display_subprojects_issues: Prikaz podprojekata na glavnom projektima (podrazumjeva se) + setting_enabled_scm: Omogući SCM (source code management) + setting_mail_handler_api_enabled: Omogući automatsku obradu ulaznih emailova + setting_mail_handler_api_key: API kljuÄ (obrada ulaznih mailova) + setting_sequential_project_identifiers: GeneriÅ¡i identifikatore projekta sekvencijalno + setting_gravatar_enabled: 'Koristi "gravatar" korisniÄke ikone' + setting_diff_max_lines_displayed: Maksimalan broj linija za prikaz razlika izmeÄ‘u dva fajla + setting_file_max_size_displayed: Maksimalna veliÄina fajla kod prikaza razlika unutar fajla (inline) + setting_repository_log_display_limit: Maksimalna veliÄina revizija prikazanih na log fajlu + setting_openid: Omogući OpenID prijavu i registraciju + + permission_edit_project: Ispravke projekta + permission_select_project_modules: Odaberi module projekta + permission_manage_members: Upravljanje Älanovima + permission_manage_versions: Upravljanje verzijama + permission_manage_categories: Upravljanje kategorijama aktivnosti + permission_add_issues: Dodaj aktivnosti + permission_edit_issues: Ispravka aktivnosti + permission_manage_issue_relations: Upravljaj relacijama meÄ‘u aktivnostima + permission_add_issue_notes: Dodaj biljeÅ¡ke + permission_edit_issue_notes: Ispravi biljeÅ¡ke + permission_edit_own_issue_notes: Ispravi sopstvene biljeÅ¡ke + permission_move_issues: Pomjeri aktivnosti + permission_delete_issues: IzbriÅ¡i aktivnosti + permission_manage_public_queries: Upravljaj javnim upitima + permission_save_queries: Snimi upite + permission_view_gantt: Pregled gantograma + permission_view_calendar: Pregled kalendara + permission_view_issue_watchers: Pregled liste korisnika koji prate aktivnost + permission_add_issue_watchers: Dodaj onoga koji prati aktivnost + permission_log_time: Evidentiraj utroÅ¡ak vremena + permission_view_time_entries: Pregled utroÅ¡ka vremena + permission_edit_time_entries: Ispravka utroÅ¡ka vremena + permission_edit_own_time_entries: Ispravka svog utroÅ¡ka vremena + permission_manage_news: Upravljaj novostima + permission_comment_news: Komentiraj novosti + permission_view_documents: Pregled dokumenata + permission_manage_files: Upravljaj fajlovima + permission_view_files: Pregled fajlova + permission_manage_wiki: Upravljaj wiki stranicama + permission_rename_wiki_pages: Ispravi wiki stranicu + permission_delete_wiki_pages: IzbriÅ¡i wiki stranicu + permission_view_wiki_pages: Pregled wiki sadržaja + permission_view_wiki_edits: Pregled wiki istorije + permission_edit_wiki_pages: Ispravka wiki stranica + permission_delete_wiki_pages_attachments: Brisanje fajlova prikaÄenih wiki-ju + permission_protect_wiki_pages: ZaÅ¡titi wiki stranicu + permission_manage_repository: Upravljaj repozitorijem + permission_browse_repository: Pregled repozitorija + permission_view_changesets: Pregled setova promjena + permission_commit_access: 'Pristup "commit"-u' + permission_manage_boards: Upravljaj forumima + permission_view_messages: Pregled poruka + permission_add_messages: Å alji poruke + permission_edit_messages: Ispravi poruke + permission_edit_own_messages: Ispravka sopstvenih poruka + permission_delete_messages: Prisanje poruka + permission_delete_own_messages: Brisanje sopstvenih poruka + + project_module_issue_tracking: Praćenje aktivnosti + project_module_time_tracking: Praćenje vremena + project_module_news: Novosti + project_module_documents: Dokumenti + project_module_files: Fajlovi + project_module_wiki: Wiki stranice + project_module_repository: Repozitorij + project_module_boards: Forumi + + label_user: Korisnik + label_user_plural: Korisnici + label_user_new: Novi korisnik + label_project: Projekat + label_project_new: Novi projekat + label_project_plural: Projekti + label_x_projects: + zero: 0 projekata + one: 1 projekat + other: "%{count} projekata" + label_project_all: Svi projekti + label_project_latest: Posljednji projekti + label_issue: Aktivnost + label_issue_new: Nova aktivnost + label_issue_plural: Aktivnosti + label_issue_view_all: Vidi sve aktivnosti + label_issues_by: "Aktivnosti po %{value}" + label_issue_added: Aktivnost je dodana + label_issue_updated: Aktivnost je izmjenjena + label_document: Dokument + label_document_new: Novi dokument + label_document_plural: Dokumenti + label_document_added: Dokument je dodan + label_role: Uloga + label_role_plural: Uloge + label_role_new: Nove uloge + label_role_and_permissions: Uloge i dozvole + label_member: IzvrÅ¡ilac + label_member_new: Novi izvrÅ¡ilac + label_member_plural: IzvrÅ¡ioci + label_tracker: PodruÄje aktivnosti + label_tracker_plural: PodruÄja aktivnosti + label_tracker_new: Novo podruÄje aktivnosti + label_workflow: Tok promjena na aktivnosti + label_issue_status: Status aktivnosti + label_issue_status_plural: Statusi aktivnosti + label_issue_status_new: Novi status + label_issue_category: Kategorija aktivnosti + label_issue_category_plural: Kategorije aktivnosti + label_issue_category_new: Nova kategorija + label_custom_field: Proizvoljno polje + label_custom_field_plural: Proizvoljna polja + label_custom_field_new: Novo proizvoljno polje + label_enumerations: Enumeracije + label_enumeration_new: Nova vrijednost + label_information: Informacija + label_information_plural: Informacije + label_please_login: Molimo prijavite se + label_register: Registracija + label_login_with_open_id_option: ili prijava sa OpenID-om + label_password_lost: Izgubljena lozinka + label_home: PoÄetna stranica + label_my_page: Moja stranica + label_my_account: Moj korisniÄki raÄun + label_my_projects: Moji projekti + label_administration: Administracija + label_login: Prijavi se + label_logout: Odjavi se + label_help: Pomoć + label_reported_issues: Prijavljene aktivnosti + label_assigned_to_me_issues: Aktivnosti dodjeljene meni + label_last_login: Posljednja konekcija + label_registered_on: Registrovan na + label_activity_plural: Promjene + label_activity: Operacija + label_overall_activity: Pregled svih promjena + label_user_activity: "Promjene izvrÅ¡ene od: %{value}" + label_new: Novi + label_logged_as: Prijavljen kao + label_environment: Sistemsko okruženje + label_authentication: Authentifikacija + label_auth_source: Mod authentifikacije + label_auth_source_new: Novi mod authentifikacije + label_auth_source_plural: Modovi authentifikacije + label_subproject_plural: Podprojekti + label_and_its_subprojects: "%{value} i njegovi podprojekti" + label_min_max_length: Min - Maks dužina + label_list: Lista + label_date: Datum + label_integer: Cijeli broj + label_float: Float + label_boolean: LogiÄka varijabla + label_string: Tekst + label_text: Dugi tekst + label_attribute: Atribut + label_attribute_plural: Atributi + label_no_data: Nema podataka za prikaz + label_change_status: Promjeni status + label_history: Istorija + label_attachment: Fajl + label_attachment_new: Novi fajl + label_attachment_delete: IzbriÅ¡i fajl + label_attachment_plural: Fajlovi + label_file_added: Fajl je dodan + label_report: IzvjeÅ¡taj + label_report_plural: IzvjeÅ¡taji + label_news: Novosti + label_news_new: Dodaj novosti + label_news_plural: Novosti + label_news_latest: Posljednje novosti + label_news_view_all: Pogledaj sve novosti + label_news_added: Novosti su dodane + label_settings: Postavke + label_overview: Pregled + label_version: Verzija + label_version_new: Nova verzija + label_version_plural: Verzije + label_confirmation: Potvrda + label_export_to: 'TakoÄ‘e dostupno u:' + label_read: ÄŒitaj... + label_public_projects: Javni projekti + label_open_issues: otvoren + label_open_issues_plural: otvoreni + label_closed_issues: zatvoren + label_closed_issues_plural: zatvoreni + label_x_open_issues_abbr_on_total: + zero: 0 otvoreno / %{total} + one: 1 otvorena / %{total} + other: "%{count} otvorene / %{total}" + label_x_open_issues_abbr: + zero: 0 otvoreno + one: 1 otvorena + other: "%{count} otvorene" + label_x_closed_issues_abbr: + zero: 0 zatvoreno + one: 1 zatvorena + other: "%{count} zatvorene" + label_total: Ukupno + label_permissions: Dozvole + label_current_status: Tekući status + label_new_statuses_allowed: Novi statusi dozvoljeni + label_all: sve + label_none: niÅ¡ta + label_nobody: niko + label_next: Sljedeće + label_previous: Predhodno + label_used_by: KoriÅ¡teno od + label_details: Detalji + label_add_note: Dodaj biljeÅ¡ku + label_per_page: Po stranici + label_calendar: Kalendar + label_months_from: mjeseci od + label_gantt: Gantt + label_internal: Interno + label_last_changes: "posljednjih %{count} promjena" + label_change_view_all: Vidi sve promjene + label_personalize_page: Personaliziraj ovu stranicu + label_comment: Komentar + label_comment_plural: Komentari + label_x_comments: + zero: bez komentara + one: 1 komentar + other: "%{count} komentari" + label_comment_add: Dodaj komentar + label_comment_added: Komentar je dodan + label_comment_delete: IzbriÅ¡i komentar + label_query: Proizvoljan upit + label_query_plural: Proizvoljni upiti + label_query_new: Novi upit + label_filter_add: Dodaj filter + label_filter_plural: Filteri + label_equals: je + label_not_equals: nije + label_in_less_than: je manji nego + label_in_more_than: je viÅ¡e nego + label_in: u + label_today: danas + label_all_time: sve vrijeme + label_yesterday: juÄe + label_this_week: ova hefta + label_last_week: zadnja hefta + label_last_n_days: "posljednjih %{count} dana" + label_this_month: ovaj mjesec + label_last_month: posljednji mjesec + label_this_year: ova godina + label_date_range: Datumski opseg + label_less_than_ago: ranije nego (dana) + label_more_than_ago: starije nego (dana) + label_ago: prije (dana) + label_contains: sadrži + label_not_contains: ne sadrži + label_day_plural: dani + label_repository: Repozitorij + label_repository_plural: Repozitoriji + label_browse: Listaj + label_revision: Revizija + label_revision_plural: Revizije + label_associated_revisions: Doddjeljene revizije + label_added: dodano + label_modified: izmjenjeno + label_copied: kopirano + label_renamed: preimenovano + label_deleted: izbrisano + label_latest_revision: Posljednja revizija + label_latest_revision_plural: Posljednje revizije + label_view_revisions: Vidi revizije + label_max_size: Maksimalna veliÄina + label_sort_highest: Pomjeri na vrh + label_sort_higher: Pomjeri gore + label_sort_lower: Pomjeri dole + label_sort_lowest: Pomjeri na dno + label_roadmap: Plan realizacije + label_roadmap_due_in: "Obavezan do %{value}" + label_roadmap_overdue: "%{value} kasni" + label_roadmap_no_issues: Nema aktivnosti za ovu verziju + label_search: Traži + label_result_plural: Rezultati + label_all_words: Sve rijeÄi + label_wiki: Wiki stranice + label_wiki_edit: ispravka wiki-ja + label_wiki_edit_plural: ispravke wiki-ja + label_wiki_page: Wiki stranica + label_wiki_page_plural: Wiki stranice + label_index_by_title: Indeks prema naslovima + label_index_by_date: Indeks po datumima + label_current_version: Tekuća verzija + label_preview: Pregled + label_feed_plural: Feeds + label_changes_details: Detalji svih promjena + label_issue_tracking: Evidencija aktivnosti + label_spent_time: UtroÅ¡ak vremena + label_f_hour: "%{value} sahat" + label_f_hour_plural: "%{value} sahata" + label_time_tracking: Evidencija vremena + label_change_plural: Promjene + label_statistics: Statistika + label_commits_per_month: '"Commit"-a po mjesecu' + label_commits_per_author: '"Commit"-a po autoru' + label_view_diff: Pregled razlika + label_diff_inline: zajedno + label_diff_side_by_side: jedna pored druge + label_options: Opcije + label_copy_workflow_from: Kopiraj tok promjena statusa iz + label_permissions_report: IzvjeÅ¡taj + label_watched_issues: Aktivnosti koje pratim + label_related_issues: Korelirane aktivnosti + label_applied_status: Status je primjenjen + label_loading: UÄitavam... + label_relation_new: Nova relacija + label_relation_delete: IzbriÅ¡i relaciju + label_relates_to: korelira sa + label_duplicates: duplikat + label_duplicated_by: duplicirano od + label_blocks: blokira + label_blocked_by: blokirano on + label_precedes: predhodi + label_follows: slijedi + label_end_to_start: 'kraj -> poÄetak' + label_end_to_end: 'kraja -> kraj' + label_start_to_start: 'poÄetak -> poÄetak' + label_start_to_end: 'poÄetak -> kraj' + label_stay_logged_in: Ostani prijavljen + label_disabled: onemogućen + label_show_completed_versions: Prikaži zavrÅ¡ene verzije + label_me: ja + label_board: Forum + label_board_new: Novi forum + label_board_plural: Forumi + label_topic_plural: Teme + label_message_plural: Poruke + label_message_last: Posljednja poruka + label_message_new: Nova poruka + label_message_posted: Poruka je dodana + label_reply_plural: Odgovori + label_send_information: PoÅ¡alji informaciju o korisniÄkom raÄunu + label_year: Godina + label_month: Mjesec + label_week: Hefta + label_date_from: Od + label_date_to: Do + label_language_based: Bazirano na korisnikovom jeziku + label_sort_by: "Sortiraj po %{value}" + label_send_test_email: PoÅ¡alji testni email + label_feeds_access_key_created_on: "Atom pristupni kljuÄ kreiran prije %{value} dana" + label_module_plural: Moduli + label_added_time_by: "Dodano od %{author} prije %{age}" + label_updated_time_by: "Izmjenjeno od %{author} prije %{age}" + label_updated_time: "Izmjenjeno prije %{value}" + label_jump_to_a_project: SkoÄi na projekat... + label_file_plural: Fajlovi + label_changeset_plural: Setovi promjena + label_default_columns: Podrazumjevane kolone + label_no_change_option: (Bez promjene) + label_bulk_edit_selected_issues: Ispravi odjednom odabrane aktivnosti + label_theme: Tema + label_default: Podrazumjevano + label_search_titles_only: Pretraži samo naslove + label_user_mail_option_all: "Za bilo koji dogaÄ‘aj na svim mojim projektima" + label_user_mail_option_selected: "Za bilo koji dogaÄ‘aj na odabranim projektima..." + label_user_mail_no_self_notified: "Ne želim notifikaciju za promjene koje sam ja napravio" + label_registration_activation_by_email: aktivacija korisniÄkog raÄuna email-om + label_registration_manual_activation: ruÄna aktivacija korisniÄkog raÄuna + label_registration_automatic_activation: automatska kreacija korisniÄkog raÄuna + label_display_per_page: "Po stranici: %{value}" + label_age: Starost + label_change_properties: Promjena osobina + label_general: Generalno + label_more: ViÅ¡e + label_scm: SCM + label_plugins: Plugin-ovi + label_ldap_authentication: LDAP authentifikacija + label_downloads_abbr: D/L + label_optional_description: Opis (opciono) + label_add_another_file: Dodaj joÅ¡ jedan fajl + label_preferences: Postavke + label_chronological_order: HronoloÅ¡ki poredak + label_reverse_chronological_order: Reverzni hronoloÅ¡ki poredak + label_planning: Planiranje + label_incoming_emails: Dolazni email-ovi + label_generate_key: GeneriÅ¡i kljuÄ + label_issue_watchers: Praćeno od + label_example: Primjer + label_display: Prikaz + + button_apply: Primjeni + button_add: Dodaj + button_archive: Arhiviranje + button_back: Nazad + button_cancel: Odustani + button_change: Izmjeni + button_change_password: Izmjena lozinke + button_check_all: OznaÄi sve + button_clear: BriÅ¡i + button_copy: Kopiraj + button_create: Novi + button_delete: BriÅ¡i + button_download: Download + button_edit: Ispravka + button_list: Lista + button_lock: ZakljuÄaj + button_log_time: UtroÅ¡ak vremena + button_login: Prijava + button_move: Pomjeri + button_rename: Promjena imena + button_reply: Odgovor + button_reset: Resetuj + button_rollback: Vrati predhodno stanje + button_save: Snimi + button_sort: Sortiranje + button_submit: PoÅ¡alji + button_test: Testiraj + button_unarchive: Otpakuj arhivu + button_uncheck_all: IskljuÄi sve + button_unlock: OtkljuÄaj + button_unwatch: Prekini notifikaciju + button_update: Promjena na aktivnosti + button_view: Pregled + button_watch: Notifikacija + button_configure: Konfiguracija + button_quote: Citat + + status_active: aktivan + status_registered: registrovan + status_locked: zakljuÄan + + text_select_mail_notifications: Odaberi dogaÄ‘aje za koje će se slati email notifikacija. + text_regexp_info: npr. ^[A-Z0-9]+$ + text_min_max_length_info: 0 znaÄi bez restrikcije + text_project_destroy_confirmation: Sigurno želite izbrisati ovaj projekat i njegove podatke ? + text_subprojects_destroy_warning: "Podprojekt(i): %{value} će takoÄ‘e biti izbrisani." + text_workflow_edit: Odaberite ulogu i podruÄje aktivnosti za ispravku toka promjena na aktivnosti + text_are_you_sure: Da li ste sigurni ? + text_tip_issue_begin_day: zadatak poÄinje danas + text_tip_issue_end_day: zadatak zavrÅ¡ava danas + text_tip_issue_begin_end_day: zadatak zapoÄinje i zavrÅ¡ava danas + text_caracters_maximum: "maksimum %{count} karaktera." + text_caracters_minimum: "Dužina mora biti najmanje %{count} znakova." + text_length_between: "Broj znakova izmeÄ‘u %{min} i %{max}." + text_tracker_no_workflow: Tok statusa nije definisan za ovo podruÄje aktivnosti + text_unallowed_characters: Nedozvoljeni znakovi + text_comma_separated: ViÅ¡estruke vrijednosti dozvoljene (odvojiti zarezom). + text_issues_ref_in_commit_messages: 'Referenciranje i zatvaranje aktivnosti putem "commit" poruka' + text_issue_added: "Aktivnost %{id} je prijavljena od %{author}." + text_issue_updated: "Aktivnost %{id} je izmjenjena od %{author}." + text_wiki_destroy_confirmation: Sigurno želite izbrisati ovaj wiki i Äitav njegov sadržaj ? + text_issue_category_destroy_question: "Neke aktivnosti (%{count}) pripadaju ovoj kategoriji. Sigurno to želite uraditi ?" + text_issue_category_destroy_assignments: Ukloni kategoriju + text_issue_category_reassign_to: Ponovo dodijeli ovu kategoriju + text_user_mail_option: "Za projekte koje niste odabrali, primićete samo notifikacije o stavkama koje pratite ili ste u njih ukljuÄeni (npr. vi ste autor ili su vama dodjeljenje)." + text_no_configuration_data: "Uloge, podruÄja aktivnosti, statusi aktivnosti i tok promjena statusa nisu konfigurisane.\nKrajnje je preporuÄeno da uÄitate tekuÄ‘e postavke. Kasnije ćete ih moći mjenjati po svojim potrebama." + text_load_default_configuration: UÄitaj tekuću konfiguraciju + text_status_changed_by_changeset: "Primjenjeno u setu promjena %{value}." + text_issues_destroy_confirmation: 'Sigurno želite izbrisati odabranu/e aktivnost/i ?' + text_select_project_modules: 'Odaberi module koje želite u ovom projektu:' + text_default_administrator_account_changed: Tekući administratorski raÄun je promjenjen + text_file_repository_writable: U direktorij sa fajlovima koji su prilozi se može pisati + text_plugin_assets_writable: U direktorij plugin-ova se može pisati + text_rmagick_available: RMagick je dostupan (opciono) + text_destroy_time_entries_question: "%{hours} sahata je prijavljeno na aktivnostima koje želite brisati. Želite li to uÄiniti ?" + text_destroy_time_entries: IzbriÅ¡i prijavljeno vrijeme + text_assign_time_entries_to_project: Dodaj prijavljenoo vrijeme projektu + text_reassign_time_entries: 'Preraspodjeli prijavljeno vrijeme na ovu aktivnost:' + text_user_wrote: "%{value} je napisao/la:" + text_enumeration_destroy_question: "Za %{count} objekata je dodjeljenja ova vrijednost." + text_enumeration_category_reassign_to: 'Ponovo im dodjeli ovu vrijednost:' + text_email_delivery_not_configured: "Email dostava nije konfiguraisana, notifikacija je onemogućena.\nKonfiguriÅ¡i SMTP server u config/configuration.yml i restartuj aplikaciju nakon toga." + text_repository_usernames_mapping: "Odaberi ili ispravi redmine korisnika mapiranog za svako korisniÄko ima naÄ‘eno u logu repozitorija.\nKorisnici sa istim imenom u redmineu i u repozitoruju se automatski mapiraju." + text_diff_truncated: '... Ovaj prikaz razlike je odsjeÄen poÅ¡to premaÅ¡uje maksimalnu veliÄinu za prikaz' + text_custom_field_possible_values_info: 'Jedna linija za svaku vrijednost' + + default_role_manager: Menadžer + default_role_developer: Programer + default_role_reporter: Reporter + default_tracker_bug: GreÅ¡ka + default_tracker_feature: Nova funkcija + default_tracker_support: PodrÅ¡ka + default_issue_status_new: Novi + default_issue_status_in_progress: In Progress + default_issue_status_resolved: RijeÅ¡en + default_issue_status_feedback: ÄŒeka se povratna informacija + default_issue_status_closed: Zatvoren + default_issue_status_rejected: Odbijen + default_doc_category_user: KorisniÄka dokumentacija + default_doc_category_tech: TehniÄka dokumentacija + default_priority_low: Nizak + default_priority_normal: Normalan + default_priority_high: Visok + default_priority_urgent: Urgentno + default_priority_immediate: Odmah + default_activity_design: Dizajn + default_activity_development: Programiranje + + enumeration_issue_priorities: Prioritet aktivnosti + enumeration_doc_categories: Kategorije dokumenata + enumeration_activities: Operacije (utroÅ¡ak vremena) + notice_unable_delete_version: Ne mogu izbrisati verziju. + button_create_and_continue: Kreiraj i nastavi + button_annotate: Zabilježi + button_activate: Aktiviraj + label_sort: Sortiranje + label_date_from_to: Od %{start} do %{end} + label_ascending: Rastuće + label_descending: Opadajuće + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: This page has %{descendants} child page(s) and descendant(s). What do you want to do? + text_wiki_page_reassign_children: Reassign child pages to this parent page + text_wiki_page_nullify_children: Keep child pages as root pages + text_wiki_page_destroy_children: Delete child pages and all their descendants + setting_password_min_length: Minimum password length + field_group_by: Group results by + mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" + label_wiki_content_added: Wiki page added + mail_subject_wiki_content_added: "'%{id}' wiki page has been added" + mail_body_wiki_content_added: The '%{id}' wiki page has been added by %{author}. + label_wiki_content_updated: Wiki page updated + mail_body_wiki_content_updated: The '%{id}' wiki page has been updated by %{author}. + permission_add_project: Create project + setting_new_project_user_role_id: Role given to a non-admin user who creates a project + label_view_all_revisions: View all revisions + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: No tracker is associated to this project. Please check the Project settings. + error_no_default_issue_status: No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses"). + text_journal_changed: "%{label} changed from %{old} to %{new}" + text_journal_set_to: "%{label} set to %{value}" + text_journal_deleted: "%{label} deleted (%{old})" + label_group_plural: Groups + label_group: Group + label_group_new: New group + label_time_entry_plural: Spent time + text_journal_added: "%{label} %{value} added" + field_active: Active + enumeration_system_activity: System Activity + permission_delete_issue_watchers: Delete watchers + version_status_closed: closed + version_status_locked: locked + version_status_open: open + error_can_not_reopen_issue_on_closed_version: An issue assigned to a closed version can not be reopened + label_user_anonymous: Anonymous + button_move_and_follow: Move and follow + setting_default_projects_modules: Default enabled modules for new projects + setting_gravatar_default: Default Gravatar image + field_sharing: Sharing + label_version_sharing_hierarchy: With project hierarchy + label_version_sharing_system: With all projects + label_version_sharing_descendants: With subprojects + label_version_sharing_tree: With project tree + label_version_sharing_none: Not shared + error_can_not_archive_project: This project can not be archived + button_duplicate: Duplicate + button_copy_and_follow: Copy and follow + label_copy_source: Source + setting_issue_done_ratio: Calculate the issue done ratio with + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + error_workflow_copy_target: Please select target tracker(s) and role(s) + setting_issue_done_ratio_issue_field: Use the issue field + label_copy_same_as_target: Same as target + label_copy_target: Target + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + label_update_issue_done_ratios: Update issue done ratios + setting_start_of_week: Start calendars on + permission_view_issues: View Issues + label_display_used_statuses_only: Only display statuses that are used by this tracker + label_revision_id: Revision %{value} + label_api_access_key: API access key + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: Atom access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_missing_api_access_key: Missing an API access key + label_missing_feeds_access_key: Missing a Atom access key + button_show: Show + text_line_separated: Multiple values allowed (one line for each value). + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: 'Enkodiranje "commit" poruka' + 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 aktivnost + one: 1 aktivnost + other: "%{count} aktivnosti" + 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.
Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: sve + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: With subprojects + label_cross_project_tree: With project tree + label_cross_project_hierarchy: With project hierarchy + label_cross_project_system: With all projects + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Ukupno + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/002a1096c30994f0a75d2638aabe3bf4e1d79ef4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/002a1096c30994f0a75d2638aabe3bf4e1d79ef4.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,66 @@ + + + + + +Wiki formatting + + + + +

Wiki記法 クイックリファレンス

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
フォントスタイル
太字*太字*太字
斜体_斜体_斜体
下線+下線+下線
å–り消ã—ç·š-å–り消ã—ç·š-å–り消ã—ç·š
??引用??引用
コード@コード@コード
整形済ã¿ãƒ†ã‚­ã‚¹ãƒˆ<pre>
 è¤‡æ•°è¡Œã®
 ã‚³ãƒ¼ãƒ‰
</pre>
+
+複数行ã®
+コード
+
+
リスト
リスト* 項目1
* é …ç›®2
  • é …ç›®1
  • é …ç›®2
é †åºä»˜ãリスト# é …ç›®1
# é …ç›®2
  1. é …ç›®1
  2. é …ç›®2
見出ã—
見出ã—1h1. タイトル1

タイトル1

見出ã—2h2. タイトル2

タイトル2

見出ã—3h3. タイトル3

タイトル3

リンク
http://foo.barhttp://foo.bar
"Foo":http://foo.barFoo
Redmine内ã®ãƒªãƒ³ã‚¯
Wikiページã¸ã®ãƒªãƒ³ã‚¯[[Wiki page]]Wiki page
ãƒã‚±ãƒƒãƒˆ #12ãƒã‚±ãƒƒãƒˆ #12
リビジョン r43リビジョン r43
commit:f30e13e43f30e13e4
source:some/filesource:some/file
ç”»åƒ
Image!ç”»åƒURL!
!添付ファイルå!
+ +

より詳細ãªãƒªãƒ•ァレンス

+ + + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/0065f42c8aab54558480736376f69f7b72cabc58.svn-base --- a/.svn/pristine/00/0065f42c8aab54558480736376f69f7b72cabc58.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ -require 'test/unit' -require 'rubygems' - -gem 'activesupport' -require 'active_support' - -gem 'actionpack' -require 'action_controller' - -gem 'mocha' -require 'mocha' - -gem 'ruby-openid' -require 'openid' - -RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT -require File.dirname(__FILE__) + "/../lib/open_id_authentication" diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/006e32915b2ea0756e649e1fb61801b416e2d647.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/006e32915b2ea0756e649e1fb61801b416e2d647.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .svn/pristine/00/009460d580c5e7ab7c5519f32165ee2250a363d7.svn-base --- a/.svn/pristine/00/009460d580c5e7ab7c5519f32165ee2250a363d7.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,229 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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::UnifiedDiffTest < ActiveSupport::TestCase - def test_subversion_diff - diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff')) - # number of files - assert_equal 4, diff.size - assert diff.detect {|file| file.file_name =~ %r{^config/settings.yml}} - end - - def test_truncate_diff - diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff'), :max_lines => 20) - assert_equal 2, diff.size - end - - def test_inline_partials - diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff')) - assert_equal 1, diff.size - diff = diff.first - assert_equal 43, diff.size - - assert_equal [51, -1], diff[0].offsets - assert_equal [51, -1], diff[1].offsets - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[1].html_line - - assert_nil diff[2].offsets - assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[2].html_line - - assert_equal [0, -14], diff[3].offsets - assert_equal [0, -14], diff[4].offsets - assert_equal 'Ut sed auctor justo', diff[3].html_line - assert_equal 'xxx auctor justo', diff[4].html_line - - assert_equal [13, -19], diff[6].offsets - assert_equal [13, -19], diff[7].offsets - - assert_equal [24, -8], diff[9].offsets - assert_equal [24, -8], diff[10].offsets - - assert_equal [37, -1], diff[12].offsets - assert_equal [37, -1], diff[13].offsets - - assert_equal [0, -38], diff[15].offsets - assert_equal [0, -38], diff[16].offsets - end - - def test_side_by_side_partials - diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff'), :type => 'sbs') - assert_equal 1, diff.size - diff = diff.first - assert_equal 32, diff.size - - assert_equal [51, -1], diff[0].offsets - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line_left - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[0].html_line_right - - assert_nil diff[1].offsets - assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_left - assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_right - - assert_equal [0, -14], diff[2].offsets - assert_equal 'Ut sed auctor justo', diff[2].html_line_left - assert_equal 'xxx auctor justo', diff[2].html_line_right - - assert_equal [13, -19], diff[4].offsets - assert_equal [24, -8], diff[6].offsets - assert_equal [37, -1], diff[8].offsets - assert_equal [0, -38], diff[10].offsets - - end - - def test_partials_with_html_entities - raw = <<-DIFF ---- test.orig.txt Wed Feb 15 16:10:39 2012 -+++ test.new.txt Wed Feb 15 16:11:25 2012 -@@ -1,5 +1,5 @@ - Semicolons were mysteriously appearing in code diffs in the repository - --void DoSomething(std::auto_ptr myObj) -+void DoSomething(const MyClass& myObj) - -DIFF - - diff = Redmine::UnifiedDiff.new(raw, :type => 'sbs') - assert_equal 1, diff.size - assert_equal 'void DoSomething(std::auto_ptr<MyClass> myObj)', diff.first[2].html_line_left - assert_equal 'void DoSomething(const MyClass& myObj)', diff.first[2].html_line_right - - diff = Redmine::UnifiedDiff.new(raw, :type => 'inline') - assert_equal 1, diff.size - assert_equal 'void DoSomething(std::auto_ptr<MyClass> myObj)', diff.first[2].html_line - assert_equal 'void DoSomething(const MyClass& myObj)', diff.first[3].html_line - end - - def test_line_starting_with_dashes - diff = Redmine::UnifiedDiff.new(<<-DIFF ---- old.txt Wed Nov 11 14:24:58 2009 -+++ new.txt Wed Nov 11 14:25:02 2009 -@@ -1,8 +1,4 @@ --Lines that starts with dashes: -- -------------------------- ---- file.c -------------------------- -+A line that starts with dashes: - - and removed. - -@@ -23,4 +19,4 @@ - - - --Another chunk of change -+Another chunk of changes - -DIFF - ) - assert_equal 1, diff.size - end - - def test_one_line_new_files - diff = Redmine::UnifiedDiff.new(<<-DIFF -diff -r 000000000000 -r ea98b14f75f0 README1 ---- /dev/null -+++ b/README1 -@@ -0,0 +1,1 @@ -+test1 -diff -r 000000000000 -r ea98b14f75f0 README2 ---- /dev/null -+++ b/README2 -@@ -0,0 +1,1 @@ -+test2 -diff -r 000000000000 -r ea98b14f75f0 README3 ---- /dev/null -+++ b/README3 -@@ -0,0 +1,3 @@ -+test4 -+test5 -+test6 -diff -r 000000000000 -r ea98b14f75f0 README4 ---- /dev/null -+++ b/README4 -@@ -0,0 +1,3 @@ -+test4 -+test5 -+test6 -DIFF - ) - assert_equal 4, diff.size - assert_equal "README1", diff[0].file_name - end - - def test_both_git_diff - diff = Redmine::UnifiedDiff.new(<<-DIFF -# HG changeset patch -# User test -# Date 1348014182 -32400 -# Node ID d1c871b8ef113df7f1c56d41e6e3bfbaff976e1f -# Parent 180b6605936cdc7909c5f08b59746ec1a7c99b3e -modify test1.txt - -diff -r 180b6605936c -r d1c871b8ef11 test1.txt ---- a/test1.txt -+++ b/test1.txt -@@ -1,1 +1,1 @@ --test1 -+modify test1 -DIFF - ) - assert_equal 1, diff.size - assert_equal "test1.txt", diff[0].file_name - end - - def test_include_a_b_slash - diff = Redmine::UnifiedDiff.new(<<-DIFF ---- test1.txt -+++ b/test02.txt -@@ -1 +0,0 @@ --modify test1 -DIFF - ) - assert_equal 1, diff.size - assert_equal "b/test02.txt", diff[0].file_name - - diff = Redmine::UnifiedDiff.new(<<-DIFF ---- a/test1.txt -+++ a/test02.txt -@@ -1 +0,0 @@ --modify test1 -DIFF - ) - assert_equal 1, diff.size - assert_equal "a/test02.txt", diff[0].file_name - - diff = Redmine::UnifiedDiff.new(<<-DIFF ---- a/test1.txt -+++ test02.txt -@@ -1 +0,0 @@ --modify test1 -DIFF - ) - assert_equal 1, diff.size - assert_equal "test02.txt", diff[0].file_name - end - - private - - def read_diff_fixture(filename) - File.new(File.join(File.dirname(__FILE__), '/../../../fixtures/diffs', filename)).read - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/0097cb632567412c9ae3b42993114578789fd825.svn-base --- a/.svn/pristine/00/0097cb632567412c9ae3b42993114578789fd825.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 VersionCustomField < CustomField - def type_name - :label_version_plural - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/00c1a9b51dc321ecb980f25b62c13dfa7fd628ff.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00c1a9b51dc321ecb980f25b62c13dfa7fd628ff.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 %>

+
+ +
<%=l(:label_attribute_plural)%> +

<%= f.text_field :attr_login, :required => true, :size => 20 %>

+

<%= f.text_field :attr_firstname, :size => 20 %>

+

<%= f.text_field :attr_lastname, :size => 20 %>

+

<%= f.text_field :attr_mail, :size => 20 %>

+
diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/00dbd874dfe947c78eb0cddc2e6a0cbc57c3f815.svn-base --- a/.svn/pristine/00/00dbd874dfe947c78eb0cddc2e6a0cbc57c3f815.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -require File.expand_path('../../../test_helper', __FILE__) - -class ApiTest::DisabledRestApiTest < ActionController::IntegrationTest - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows - - def setup - Setting.rest_api_enabled = '0' - Setting.login_required = '1' - end - - def teardown - Setting.rest_api_enabled = '1' - Setting.login_required = '0' - end - - def test_with_a_valid_api_token - @user = User.generate! - @token = Token.create!(:user => @user, :action => 'api') - - get "/news.xml?key=#{@token.value}" - assert_response :unauthorized - assert_equal User.anonymous, User.current - - get "/news.json?key=#{@token.value}" - assert_response :unauthorized - assert_equal User.anonymous, User.current - end - - def test_with_valid_username_password_http_authentication - @user = User.generate! do |user| - user.password = 'my_password' - end - - get "/news.xml", nil, credentials(@user.login, 'my_password') - assert_response :unauthorized - assert_equal User.anonymous, User.current - - get "/news.json", nil, credentials(@user.login, 'my_password') - assert_response :unauthorized - assert_equal User.anonymous, User.current - end - - def test_with_valid_token_http_authentication - @user = User.generate! - @token = Token.create!(:user => @user, :action => 'api') - - get "/news.xml", nil, credentials(@token.value, 'X') - assert_response :unauthorized - assert_equal User.anonymous, User.current - - get "/news.json", nil, credentials(@token.value, 'X') - assert_response :unauthorized - assert_equal User.anonymous, User.current - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/00e064a58814bd1506484c9e41b6bc1f8553a834.svn-base --- a/.svn/pristine/00/00e064a58814bd1506484c9e41b6bc1f8553a834.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,795 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ApiTest::IssuesTest < ActionController::IntegrationTest - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :issue_relations, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries, - :attachments - - def setup - Setting.rest_api_enabled = '1' - end - - context "/issues" do - # Use a private project to make sure auth is really working and not just - # only showing public issues. - should_allow_api_authentication(:get, "/projects/private-child/issues.xml") - - should "contain metadata" do - get '/issues.xml' - - assert_tag :tag => 'issues', - :attributes => { - :type => 'array', - :total_count => assigns(:issue_count), - :limit => 25, - :offset => 0 - } - end - - context "with offset and limit" do - should "use the params" do - get '/issues.xml?offset=2&limit=3' - - assert_equal 3, assigns(:limit) - assert_equal 2, assigns(:offset) - assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}} - end - end - - context "with nometa param" do - should "not contain metadata" do - get '/issues.xml?nometa=1' - - assert_tag :tag => 'issues', - :attributes => { - :type => 'array', - :total_count => nil, - :limit => nil, - :offset => nil - } - end - end - - context "with nometa header" do - should "not contain metadata" do - get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'} - - assert_tag :tag => 'issues', - :attributes => { - :type => 'array', - :total_count => nil, - :limit => nil, - :offset => nil - } - end - end - - context "with relations" do - should "display relations" do - get '/issues.xml?include=relations' - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag 'relations', - :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}}, - :children => {:count => 1}, - :child => { - :tag => 'relation', - :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', - :relation_type => 'relates'} - } - assert_tag 'relations', - :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}}, - :children => {:count => 0} - end - end - - context "with invalid query params" do - should "return errors" do - get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}} - - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"} - end - end - - context "with custom field filter" do - should "show only issues with the custom field value" do - get '/issues.xml', - {:set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, - :v => {:cf_1 => ['MySQL']}} - expected_ids = Issue.visible.all( - :include => :custom_values, - :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id) - assert_select 'issues > issue > id', :count => expected_ids.count do |ids| - ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } - end - end - end - - context "with custom field filter (shorthand method)" do - should "show only issues with the custom field value" do - get '/issues.xml', { :cf_1 => 'MySQL' } - - expected_ids = Issue.visible.all( - :include => :custom_values, - :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id) - - assert_select 'issues > issue > id', :count => expected_ids.count do |ids| - ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } - end - end - end - end - - context "/index.json" do - should_allow_api_authentication(:get, "/projects/private-child/issues.json") - end - - context "/index.xml with filter" do - should "show only issues with the status_id" do - get '/issues.xml?status_id=5' - - expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id) - - assert_select 'issues > issue > id', :count => expected_ids.count do |ids| - ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } - end - end - end - - context "/index.json with filter" do - should "show only issues with the status_id" do - get '/issues.json?status_id=5' - - json = ActiveSupport::JSON.decode(response.body) - status_ids_used = json['issues'].collect {|j| j['status']['id'] } - assert_equal 3, status_ids_used.length - assert status_ids_used.all? {|id| id == 5 } - end - - end - - # Issue 6 is on a private project - context "/issues/6.xml" do - should_allow_api_authentication(:get, "/issues/6.xml") - end - - context "/issues/6.json" do - should_allow_api_authentication(:get, "/issues/6.json") - end - - context "GET /issues/:id" do - context "with journals" do - context ".xml" do - should "display journals" do - get '/issues/1.xml?include=journals' - - assert_tag :tag => 'issue', - :child => { - :tag => 'journals', - :attributes => { :type => 'array' }, - :child => { - :tag => 'journal', - :attributes => { :id => '1'}, - :child => { - :tag => 'details', - :attributes => { :type => 'array' }, - :child => { - :tag => 'detail', - :attributes => { :name => 'status_id' }, - :child => { - :tag => 'old_value', - :content => '1', - :sibling => { - :tag => 'new_value', - :content => '2' - } - } - } - } - } - } - end - end - end - - context "with custom fields" do - context ".xml" do - should "display custom fields" do - get '/issues/3.xml' - - assert_tag :tag => 'issue', - :child => { - :tag => 'custom_fields', - :attributes => { :type => 'array' }, - :child => { - :tag => 'custom_field', - :attributes => { :id => '1'}, - :child => { - :tag => 'value', - :content => 'MySQL' - } - } - } - - assert_nothing_raised do - Hash.from_xml(response.body).to_xml - end - end - end - end - - context "with multi custom fields" do - setup do - field = CustomField.find(1) - field.update_attribute :multiple, true - issue = Issue.find(3) - issue.custom_field_values = {1 => ['MySQL', 'Oracle']} - issue.save! - end - - context ".xml" do - should "display custom fields" do - get '/issues/3.xml' - assert_response :success - assert_tag :tag => 'issue', - :child => { - :tag => 'custom_fields', - :attributes => { :type => 'array' }, - :child => { - :tag => 'custom_field', - :attributes => { :id => '1'}, - :child => { - :tag => 'value', - :attributes => { :type => 'array' }, - :children => { :count => 2 } - } - } - } - - xml = Hash.from_xml(response.body) - custom_fields = xml['issue']['custom_fields'] - assert_kind_of Array, custom_fields - field = custom_fields.detect {|f| f['id'] == '1'} - assert_kind_of Hash, field - assert_equal ['MySQL', 'Oracle'], field['value'].sort - end - end - - context ".json" do - should "display custom fields" do - get '/issues/3.json' - assert_response :success - json = ActiveSupport::JSON.decode(response.body) - custom_fields = json['issue']['custom_fields'] - assert_kind_of Array, custom_fields - field = custom_fields.detect {|f| f['id'] == 1} - assert_kind_of Hash, field - assert_equal ['MySQL', 'Oracle'], field['value'].sort - end - end - end - - context "with empty value for multi custom field" do - setup do - field = CustomField.find(1) - field.update_attribute :multiple, true - issue = Issue.find(3) - issue.custom_field_values = {1 => ['']} - issue.save! - end - - context ".xml" do - should "display custom fields" do - get '/issues/3.xml' - assert_response :success - assert_tag :tag => 'issue', - :child => { - :tag => 'custom_fields', - :attributes => { :type => 'array' }, - :child => { - :tag => 'custom_field', - :attributes => { :id => '1'}, - :child => { - :tag => 'value', - :attributes => { :type => 'array' }, - :children => { :count => 0 } - } - } - } - - xml = Hash.from_xml(response.body) - custom_fields = xml['issue']['custom_fields'] - assert_kind_of Array, custom_fields - field = custom_fields.detect {|f| f['id'] == '1'} - assert_kind_of Hash, field - assert_equal [], field['value'] - end - end - - context ".json" do - should "display custom fields" do - get '/issues/3.json' - assert_response :success - json = ActiveSupport::JSON.decode(response.body) - custom_fields = json['issue']['custom_fields'] - assert_kind_of Array, custom_fields - field = custom_fields.detect {|f| f['id'] == 1} - assert_kind_of Hash, field - assert_equal [], field['value'].sort - end - end - end - - context "with attachments" do - context ".xml" do - should "display attachments" do - get '/issues/3.xml?include=attachments' - - assert_tag :tag => 'issue', - :child => { - :tag => 'attachments', - :children => {:count => 5}, - :child => { - :tag => 'attachment', - :child => { - :tag => 'filename', - :content => 'source.rb', - :sibling => { - :tag => 'content_url', - :content => 'http://www.example.com/attachments/download/4/source.rb' - } - } - } - } - end - end - end - - context "with subtasks" do - setup do - @c1 = Issue.create!( - :status_id => 1, :subject => "child c1", - :tracker_id => 1, :project_id => 1, :author_id => 1, - :parent_issue_id => 1 - ) - @c2 = Issue.create!( - :status_id => 1, :subject => "child c2", - :tracker_id => 1, :project_id => 1, :author_id => 1, - :parent_issue_id => 1 - ) - @c3 = Issue.create!( - :status_id => 1, :subject => "child c3", - :tracker_id => 1, :project_id => 1, :author_id => 1, - :parent_issue_id => @c1.id - ) - end - - context ".xml" do - should "display children" do - get '/issues/1.xml?include=children' - - assert_tag :tag => 'issue', - :child => { - :tag => 'children', - :children => {:count => 2}, - :child => { - :tag => 'issue', - :attributes => {:id => @c1.id.to_s}, - :child => { - :tag => 'subject', - :content => 'child c1', - :sibling => { - :tag => 'children', - :children => {:count => 1}, - :child => { - :tag => 'issue', - :attributes => {:id => @c3.id.to_s} - } - } - } - } - } - end - - context ".json" do - should "display children" do - get '/issues/1.json?include=children' - - json = ActiveSupport::JSON.decode(response.body) - assert_equal([ - { - 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'}, - 'children' => [{'id' => @c3.id, 'subject' => 'child c3', - 'tracker' => {'id' => 1, 'name' => 'Bug'} }] - }, - { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} } - ], - json['issue']['children']) - end - end - end - end - end - - context "POST /issues.xml" do - should_allow_api_authentication( - :post, - '/issues.xml', - {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, - {:success_code => :created} - ) - should "create an issue with the attributes" do - assert_difference('Issue.count') do - post '/issues.xml', - {:issue => {:project_id => 1, :subject => 'API test', - :tracker_id => 2, :status_id => 3}}, credentials('jsmith') - end - issue = Issue.first(:order => 'id DESC') - assert_equal 1, issue.project_id - assert_equal 2, issue.tracker_id - assert_equal 3, issue.status_id - assert_equal 'API test', issue.subject - - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s} - end - end - - context "POST /issues.xml with failure" do - should "have an errors tag" do - assert_no_difference('Issue.count') do - post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith') - end - - assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} - end - end - - context "POST /issues.json" do - should_allow_api_authentication(:post, - '/issues.json', - {:issue => {:project_id => 1, :subject => 'API test', - :tracker_id => 2, :status_id => 3}}, - {:success_code => :created}) - - should "create an issue with the attributes" do - assert_difference('Issue.count') do - post '/issues.json', - {:issue => {:project_id => 1, :subject => 'API test', - :tracker_id => 2, :status_id => 3}}, - credentials('jsmith') - end - - issue = Issue.first(:order => 'id DESC') - assert_equal 1, issue.project_id - assert_equal 2, issue.tracker_id - assert_equal 3, issue.status_id - assert_equal 'API test', issue.subject - end - - end - - context "POST /issues.json with failure" do - should "have an errors element" do - assert_no_difference('Issue.count') do - post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith') - end - - json = ActiveSupport::JSON.decode(response.body) - assert json['errors'].include?("Subject can't be blank") - end - end - - # Issue 6 is on a private project - context "PUT /issues/6.xml" do - setup do - @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}} - end - - should_allow_api_authentication(:put, - '/issues/6.xml', - {:issue => {:subject => 'API update', :notes => 'A new note'}}, - {:success_code => :ok}) - - should "not create a new issue" do - assert_no_difference('Issue.count') do - put '/issues/6.xml', @parameters, credentials('jsmith') - end - end - - should "create a new journal" do - assert_difference('Journal.count') do - put '/issues/6.xml', @parameters, credentials('jsmith') - end - end - - should "add the note to the journal" do - put '/issues/6.xml', @parameters, credentials('jsmith') - - journal = Journal.last - assert_equal "A new note", journal.notes - end - - should "update the issue" do - put '/issues/6.xml', @parameters, credentials('jsmith') - - issue = Issue.find(6) - assert_equal "API update", issue.subject - end - - end - - context "PUT /issues/3.xml with custom fields" do - setup do - @parameters = { - :issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, - {'id' => '2', 'value' => '150'}]} - } - end - - should "update custom fields" do - assert_no_difference('Issue.count') do - put '/issues/3.xml', @parameters, credentials('jsmith') - end - - issue = Issue.find(3) - assert_equal '150', issue.custom_value_for(2).value - assert_equal 'PostgreSQL', issue.custom_value_for(1).value - end - end - - context "PUT /issues/3.xml with multi custom fields" do - setup do - field = CustomField.find(1) - field.update_attribute :multiple, true - @parameters = { - :issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, - {'id' => '2', 'value' => '150'}]} - } - end - - should "update custom fields" do - assert_no_difference('Issue.count') do - put '/issues/3.xml', @parameters, credentials('jsmith') - end - - issue = Issue.find(3) - assert_equal '150', issue.custom_value_for(2).value - assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort - end - end - - context "PUT /issues/3.xml with project change" do - setup do - @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}} - end - - should "update project" do - assert_no_difference('Issue.count') do - put '/issues/3.xml', @parameters, credentials('jsmith') - end - - issue = Issue.find(3) - assert_equal 2, issue.project_id - assert_equal 'Project changed', issue.subject - end - end - - context "PUT /issues/6.xml with failed update" do - setup do - @parameters = {:issue => {:subject => ''}} - end - - should "not create a new issue" do - assert_no_difference('Issue.count') do - put '/issues/6.xml', @parameters, credentials('jsmith') - end - end - - should "not create a new journal" do - assert_no_difference('Journal.count') do - put '/issues/6.xml', @parameters, credentials('jsmith') - end - end - - should "have an errors tag" do - put '/issues/6.xml', @parameters, credentials('jsmith') - - assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} - end - end - - context "PUT /issues/6.json" do - setup do - @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}} - end - - should_allow_api_authentication(:put, - '/issues/6.json', - {:issue => {:subject => 'API update', :notes => 'A new note'}}, - {:success_code => :ok}) - - should "update the issue" do - assert_no_difference('Issue.count') do - assert_difference('Journal.count') do - put '/issues/6.json', @parameters, credentials('jsmith') - - assert_response :ok - assert_equal '', response.body - end - end - - issue = Issue.find(6) - assert_equal "API update", issue.subject - journal = Journal.last - assert_equal "A new note", journal.notes - end - end - - context "PUT /issues/6.json with failed update" do - should "return errors" do - assert_no_difference('Issue.count') do - assert_no_difference('Journal.count') do - put '/issues/6.json', {:issue => {:subject => ''}}, credentials('jsmith') - - assert_response :unprocessable_entity - end - end - - json = ActiveSupport::JSON.decode(response.body) - assert json['errors'].include?("Subject can't be blank") - end - end - - context "DELETE /issues/1.xml" do - should_allow_api_authentication(:delete, - '/issues/6.xml', - {}, - {:success_code => :ok}) - - should "delete the issue" do - assert_difference('Issue.count', -1) do - delete '/issues/6.xml', {}, credentials('jsmith') - - assert_response :ok - assert_equal '', response.body - end - - assert_nil Issue.find_by_id(6) - end - end - - context "DELETE /issues/1.json" do - should_allow_api_authentication(:delete, - '/issues/6.json', - {}, - {:success_code => :ok}) - - should "delete the issue" do - assert_difference('Issue.count', -1) do - delete '/issues/6.json', {}, credentials('jsmith') - - assert_response :ok - assert_equal '', response.body - end - - assert_nil Issue.find_by_id(6) - end - end - - def test_create_issue_with_uploaded_file - set_tmp_attachments_directory - # upload the file - assert_difference 'Attachment.count' do - post '/uploads.xml', 'test_create_with_upload', - {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) - assert_response :created - end - xml = Hash.from_xml(response.body) - token = xml['upload']['token'] - attachment = Attachment.first(:order => 'id DESC') - - # create the issue with the upload's token - assert_difference 'Issue.count' do - post '/issues.xml', - {:issue => {:project_id => 1, :subject => 'Uploaded file', - :uploads => [{:token => token, :filename => 'test.txt', - :content_type => 'text/plain'}]}}, - credentials('jsmith') - assert_response :created - end - issue = Issue.first(:order => 'id DESC') - assert_equal 1, issue.attachments.count - assert_equal attachment, issue.attachments.first - - attachment.reload - assert_equal 'test.txt', attachment.filename - assert_equal 'text/plain', attachment.content_type - assert_equal 'test_create_with_upload'.size, attachment.filesize - assert_equal 2, attachment.author_id - - # get the issue with its attachments - get "/issues/#{issue.id}.xml", :include => 'attachments' - assert_response :success - xml = Hash.from_xml(response.body) - attachments = xml['issue']['attachments'] - assert_kind_of Array, attachments - assert_equal 1, attachments.size - url = attachments.first['content_url'] - assert_not_nil url - - # download the attachment - get url - assert_response :success - end - - def test_update_issue_with_uploaded_file - set_tmp_attachments_directory - # upload the file - assert_difference 'Attachment.count' do - post '/uploads.xml', 'test_upload_with_upload', - {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) - assert_response :created - end - xml = Hash.from_xml(response.body) - token = xml['upload']['token'] - attachment = Attachment.first(:order => 'id DESC') - - # update the issue with the upload's token - assert_difference 'Journal.count' do - put '/issues/1.xml', - {:issue => {:notes => 'Attachment added', - :uploads => [{:token => token, :filename => 'test.txt', - :content_type => 'text/plain'}]}}, - credentials('jsmith') - assert_response :ok - assert_equal '', @response.body - end - - issue = Issue.find(1) - assert_include attachment, issue.attachments - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/00/00e6c0514da25c76abd6063e97c17125ab6f9899.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00e6c0514da25c76abd6063e97c17125ab6f9899.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'TuÄné'; +jsToolBar.strings['Italic'] = 'Kurzíva'; +jsToolBar.strings['Underline'] = 'PodÄiarknuté'; +jsToolBar.strings['Deleted'] = 'PreÅ¡krtnuté'; +jsToolBar.strings['Code'] = 'Zobrazenie kódu'; +jsToolBar.strings['Heading 1'] = 'Nadpis 1'; +jsToolBar.strings['Heading 2'] = 'Nadpis 2'; +jsToolBar.strings['Heading 3'] = 'Nadpis 3'; +jsToolBar.strings['Unordered list'] = 'Odrážkový zoznam'; +jsToolBar.strings['Ordered list'] = 'Číslovaný zoznam'; +jsToolBar.strings['Quote'] = 'Odsadenie'; +jsToolBar.strings['Unquote'] = 'ZruÅ¡iÅ¥ odsadenie'; +jsToolBar.strings['Preformatted text'] = 'Predformátovaný text'; +jsToolBar.strings['Wiki link'] = 'Odkaz na wikistránku'; +jsToolBar.strings['Image'] = 'Obrázok'; diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/01295ecfb41026f1bdc485aae7fd6a8c5187ac83.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01295ecfb41026f1bdc485aae7fd6a8c5187ac83.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,58 @@ +
+<%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %> +
+ +

<%=l(:label_user_plural)%>

+ +<%= form_tag(users_path, :method => :get) do %> +
<%= l(:label_filter_plural) %> + +<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> + +<% if @groups.present? %> + +<%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %> +<% end %> + + +<%= text_field_tag 'name', params[:name], :size => 30 %> +<%= submit_tag l(:button_apply), :class => "small", :name => nil %> +<%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %> +
+<% end %> +  + +
+ + + <%= sort_header_tag('login', :caption => l(:field_login)) %> + <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %> + <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %> + <%= sort_header_tag('mail', :caption => l(:field_mail)) %> + <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %> + + + +<% for user in @users -%> + "> + + + + + + + + + +<% end -%> + +
<%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %><%= h(user.firstname) %><%= h(user.lastname) %><%= checked_image user.admin? %><%= format_time(user.created_on) %> + <%= 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 d98d22a98252 -r fb9a13467253 .svn/pristine/01/013035c9e2d7a0734359dad136ab782f7272366b.svn-base --- a/.svn/pristine/01/013035c9e2d7a0734359dad136ab782f7272366b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -class CreateJournals < ActiveRecord::Migration - - # model removed, but needed for data migration - class IssueHistory < ActiveRecord::Base; belongs_to :issue; end - # model removed - class Permission < ActiveRecord::Base; end - - def self.up - create_table :journals, :force => true do |t| - t.column "journalized_id", :integer, :default => 0, :null => false - t.column "journalized_type", :string, :limit => 30, :default => "", :null => false - t.column "user_id", :integer, :default => 0, :null => false - t.column "notes", :text - t.column "created_on", :datetime, :null => false - end - create_table :journal_details, :force => true do |t| - t.column "journal_id", :integer, :default => 0, :null => false - t.column "property", :string, :limit => 30, :default => "", :null => false - t.column "prop_key", :string, :limit => 30, :default => "", :null => false - t.column "old_value", :string - t.column "value", :string - end - - # indexes - add_index "journals", ["journalized_id", "journalized_type"], :name => "journals_journalized_id" - add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id" - - Permission.create :controller => "issues", :action => "history", :description => "label_history", :sort => 1006, :is_public => true, :mail_option => 0, :mail_enabled => 0 - - # data migration - IssueHistory.find(:all, :include => :issue).each {|h| - j = Journal.new(:journalized => h.issue, :user_id => h.author_id, :notes => h.notes, :created_on => h.created_on) - j.details << JournalDetail.new(:property => 'attr', :prop_key => 'status_id', :value => h.status_id) - j.save - } - - drop_table :issue_histories - end - - def self.down - drop_table :journal_details - drop_table :journals - - create_table "issue_histories", :force => true do |t| - t.column "issue_id", :integer, :default => 0, :null => false - t.column "status_id", :integer, :default => 0, :null => false - t.column "author_id", :integer, :default => 0, :null => false - t.column "notes", :text, :default => "" - t.column "created_on", :timestamp - end - - add_index "issue_histories", ["issue_id"], :name => "issue_histories_issue_id" - - Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'history']).destroy - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/0140b2e0d97ff88f8ef3743106e70b9d5c56caec.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/0140b2e0d97ff88f8ef3743106e70b9d5c56caec.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,96 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 AuthSourcesController < ApplicationController + layout 'admin' + menu_item :ldap_authentication + + before_filter :require_admin + before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy] + + def index + @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25 + end + + def new + klass_name = params[:type] || 'AuthSourceLdap' + @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source]) + render_404 unless @auth_source + end + + def create + @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source]) + if @auth_source.save + flash[:notice] = l(:notice_successful_create) + redirect_to auth_sources_path + else + render :action => 'new' + end + end + + def edit + end + + def update + if @auth_source.update_attributes(params[:auth_source]) + flash[:notice] = l(:notice_successful_update) + redirect_to auth_sources_path + else + render :action => 'edit' + end + end + + def test_connection + begin + @auth_source.test_connection + flash[:notice] = l(:notice_successful_connection) + rescue Exception => e + flash[:error] = l(:error_unable_to_connect, e.message) + end + redirect_to auth_sources_path + end + + def destroy + unless @auth_source.users.exists? + @auth_source.destroy + flash[:notice] = l(:notice_successful_delete) + end + redirect_to auth_sources_path + end + + def autocomplete_for_new_user + results = AuthSource.search(params[:term]) + + render :json => results.map {|result| { + 'value' => result[:login], + 'label' => "#{result[:login]} (#{result[:firstname]} #{result[:lastname]})", + 'login' => result[:login].to_s, + 'firstname' => result[:firstname].to_s, + 'lastname' => result[:lastname].to_s, + 'mail' => result[:mail].to_s, + 'auth_source_id' => result[:auth_source_id].to_s + }} + end + + private + + def find_auth_source + @auth_source = AuthSource.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/018db29c39216709e43cc1f869161f57aa497455.svn-base --- a/.svn/pristine/01/018db29c39216709e43cc1f869161f57aa497455.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CustomFieldValue - attr_accessor :custom_field, :customized, :value - - def custom_field_id - custom_field.id - end - - def true? - self.value == '1' - end - - def editable? - custom_field.editable? - end - - def visible? - custom_field.visible? - end - - def required? - custom_field.is_required? - end - - def to_s - value.to_s - end - - def validate_value - custom_field.validate_field_value(value).each do |message| - customized.errors.add(:base, custom_field.name + ' ' + message) - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/01d08f5b37dfa46aae097b270dbf10baa02d7e48.svn-base --- a/.svn/pristine/01/01d08f5b37dfa46aae097b270dbf10baa02d7e48.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -class ExportPdf < ActiveRecord::Migration - # model removed - class Permission < ActiveRecord::Base; end - - def self.up - Permission.create :controller => "projects", :action => "export_issues_pdf", :description => "label_export_pdf", :sort => 1002, :is_public => true, :mail_option => 0, :mail_enabled => 0 - Permission.create :controller => "issues", :action => "export_pdf", :description => "label_export_pdf", :sort => 1015, :is_public => true, :mail_option => 0, :mail_enabled => 0 - end - - def self.down - Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'export_issues_pdf']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'export_pdf']).destroy - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/01df356dd5562f68f76338bddccfd73e2d039ee5.svn-base --- a/.svn/pristine/01/01df356dd5562f68f76338bddccfd73e2d039ee5.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Acts - module Attachable - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def acts_as_attachable(options = {}) - cattr_accessor :attachable_options - self.attachable_options = {} - attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym - attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym - - has_many :attachments, options.merge(:as => :container, - :order => "#{Attachment.table_name}.created_on ASC, #{Attachment.table_name}.id ASC", - :dependent => :destroy) - send :include, Redmine::Acts::Attachable::InstanceMethods - before_save :attach_saved_attachments - end - end - - module InstanceMethods - def self.included(base) - base.extend ClassMethods - end - - def attachments_visible?(user=User.current) - (respond_to?(:visible?) ? visible?(user) : true) && - user.allowed_to?(self.class.attachable_options[:view_permission], self.project) - end - - def attachments_deletable?(user=User.current) - (respond_to?(:visible?) ? visible?(user) : true) && - user.allowed_to?(self.class.attachable_options[:delete_permission], self.project) - end - - def saved_attachments - @saved_attachments ||= [] - end - - def unsaved_attachments - @unsaved_attachments ||= [] - end - - def save_attachments(attachments, author=User.current) - if attachments.is_a?(Hash) - attachments = attachments.stringify_keys - attachments = attachments.to_a.sort {|a, b| - if a.first.to_i > 0 && b.first.to_i > 0 - a.first.to_i <=> b.first.to_i - elsif a.first.to_i > 0 - 1 - elsif b.first.to_i > 0 - -1 - else - a.first <=> b.first - end - } - attachments = attachments.map(&:last) - end - if attachments.is_a?(Array) - attachments.each do |attachment| - a = nil - if file = attachment['file'] - next unless file.size > 0 - a = Attachment.create(:file => file, :author => author) - elsif token = attachment['token'] - a = Attachment.find_by_token(token) - next unless a - a.filename = attachment['filename'] unless attachment['filename'].blank? - a.content_type = attachment['content_type'] - end - next unless a - a.description = attachment['description'].to_s.strip - if a.new_record? - unsaved_attachments << a - else - saved_attachments << a - end - end - end - {:files => saved_attachments, :unsaved => unsaved_attachments} - end - - def attach_saved_attachments - saved_attachments.each do |attachment| - self.attachments << attachment - end - end - - module ClassMethods - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/01e977490126a0e666397a535bfda934cac273ac.svn-base --- a/.svn/pristine/01/01e977490126a0e666397a535bfda934cac273ac.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,435 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ScmFetchError < Exception; end - -class Repository < ActiveRecord::Base - include Redmine::Ciphering - include Redmine::SafeAttributes - - # Maximum length for repository identifiers - IDENTIFIER_MAX_LENGTH = 255 - - belongs_to :project - has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" - has_many :filechanges, :class_name => 'Change', :through => :changesets - - serialize :extra_info - - before_save :check_default - - # Raw SQL to delete changesets and changes in the database - # has_many :changesets, :dependent => :destroy is too slow for big repositories - before_destroy :clear_changesets - - validates_length_of :password, :maximum => 255, :allow_nil => true - validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true - validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? } - validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true - validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph) - # donwcase letters, digits, dashes, underscores but not digits only - validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true - # Checks if the SCM is enabled when creating a repository - validate :repo_create_validation, :on => :create - - safe_attributes 'identifier', - 'login', - 'password', - 'path_encoding', - 'log_encoding', - 'is_default' - - safe_attributes 'url', - :if => lambda {|repository, user| repository.new_record?} - - def repo_create_validation - unless Setting.enabled_scm.include?(self.class.name.demodulize) - errors.add(:type, :invalid) - end - end - - def self.human_attribute_name(attribute_key_name, *args) - attr_name = attribute_key_name.to_s - if attr_name == "log_encoding" - attr_name = "commit_logs_encoding" - end - super(attr_name, *args) - end - - # Removes leading and trailing whitespace - def url=(arg) - write_attribute(:url, arg ? arg.to_s.strip : nil) - end - - # Removes leading and trailing whitespace - def root_url=(arg) - write_attribute(:root_url, arg ? arg.to_s.strip : nil) - end - - def password - read_ciphered_attribute(:password) - end - - def password=(arg) - write_ciphered_attribute(:password, arg) - end - - def scm_adapter - self.class.scm_adapter_class - end - - def scm - unless @scm - @scm = self.scm_adapter.new(url, root_url, - login, password, path_encoding) - if root_url.blank? && @scm.root_url.present? - update_attribute(:root_url, @scm.root_url) - end - end - @scm - end - - def scm_name - self.class.scm_name - end - - def name - if identifier.present? - identifier - elsif is_default? - l(:field_repository_is_default) - else - scm_name - end - end - - def identifier=(identifier) - super unless identifier_frozen? - end - - def identifier_frozen? - errors[:identifier].blank? && !(new_record? || identifier.blank?) - end - - def identifier_param - if is_default? - nil - elsif identifier.present? - identifier - else - id.to_s - end - end - - def <=>(repository) - if is_default? - -1 - elsif repository.is_default? - 1 - else - identifier.to_s <=> repository.identifier.to_s - end - end - - def self.find_by_identifier_param(param) - if param.to_s =~ /^\d+$/ - find_by_id(param) - else - find_by_identifier(param) - end - end - - def merge_extra_info(arg) - h = extra_info || {} - return h if arg.nil? - h.merge!(arg) - write_attribute(:extra_info, h) - end - - def report_last_commit - true - end - - def supports_cat? - scm.supports_cat? - end - - def supports_annotate? - scm.supports_annotate? - end - - def supports_all_revisions? - true - end - - def supports_directory_revisions? - false - end - - def supports_revision_graph? - false - end - - def entry(path=nil, identifier=nil) - scm.entry(path, identifier) - end - - def entries(path=nil, identifier=nil) - entries = scm.entries(path, identifier) - load_entries_changesets(entries) - entries - end - - def branches - scm.branches - end - - def tags - scm.tags - end - - def default_branch - nil - end - - def properties(path, identifier=nil) - scm.properties(path, identifier) - end - - def cat(path, identifier=nil) - scm.cat(path, identifier) - end - - def diff(path, rev, rev_to) - scm.diff(path, rev, rev_to) - end - - def diff_format_revisions(cs, cs_to, sep=':') - text = "" - text << cs_to.format_identifier + sep if cs_to - text << cs.format_identifier if cs - text - end - - # Returns a path relative to the url of the repository - def relative_path(path) - path - end - - # Finds and returns a revision with a number or the beginning of a hash - def find_changeset_by_name(name) - return nil if name.blank? - s = name.to_s - changesets.find(:first, :conditions => (s.match(/^\d*$/) ? - ["revision = ?", s] : ["revision LIKE ?", s + '%'])) - end - - def latest_changeset - @latest_changeset ||= changesets.find(:first) - end - - # Returns the latest changesets for +path+ - # Default behaviour is to search in cached changesets - def latest_changesets(path, rev, limit=10) - if path.blank? - changesets.find( - :all, - :include => :user, - :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", - :limit => limit) - else - filechanges.find( - :all, - :include => {:changeset => :user}, - :conditions => ["path = ?", path.with_leading_slash], - :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", - :limit => limit - ).collect(&:changeset) - end - end - - def scan_changesets_for_issue_ids - self.changesets.each(&:scan_comment_for_issue_ids) - end - - # Returns an array of committers usernames and associated user_id - def committers - @committers ||= Changeset.connection.select_rows( - "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}") - end - - # Maps committers username to a user ids - def committer_ids=(h) - if h.is_a?(Hash) - committers.each do |committer, user_id| - new_user_id = h[committer] - if new_user_id && (new_user_id.to_i != user_id.to_i) - new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil) - Changeset.update_all( - "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", - ["repository_id = ? AND committer = ?", id, committer]) - end - end - @committers = nil - @found_committer_users = nil - true - else - false - end - end - - # Returns the Redmine User corresponding to the given +committer+ - # It will return nil if the committer is not yet mapped and if no User - # with the same username or email was found - def find_committer_user(committer) - unless committer.blank? - @found_committer_users ||= {} - return @found_committer_users[committer] if @found_committer_users.has_key?(committer) - - user = nil - c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user) - if c && c.user - user = c.user - elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/ - username, email = $1.strip, $3 - u = User.find_by_login(username) - u ||= User.find_by_mail(email) unless email.blank? - user = u - end - @found_committer_users[committer] = user - user - end - end - - def repo_log_encoding - encoding = log_encoding.to_s.strip - encoding.blank? ? 'UTF-8' : encoding - end - - # Fetches new changesets for all repositories of active projects - # Can be called periodically by an external script - # eg. ruby script/runner "Repository.fetch_changesets" - def self.fetch_changesets - Project.active.has_module(:repository).all.each do |project| - project.repositories.each do |repository| - begin - repository.fetch_changesets - rescue Redmine::Scm::Adapters::CommandFailed => e - logger.error "scm: error during fetching changesets: #{e.message}" - end - end - end - end - - # scan changeset comments to find related and fixed issues for all repositories - def self.scan_changesets_for_issue_ids - find(:all).each(&:scan_changesets_for_issue_ids) - end - - def self.scm_name - 'Abstract' - end - - def self.available_scm - subclasses.collect {|klass| [klass.scm_name, klass.name]} - end - - def self.factory(klass_name, *args) - klass = "Repository::#{klass_name}".constantize - klass.new(*args) - rescue - nil - end - - def self.scm_adapter_class - nil - end - - def self.scm_command - ret = "" - begin - ret = self.scm_adapter_class.client_command if self.scm_adapter_class - rescue Exception => e - logger.error "scm: error during get command: #{e.message}" - end - ret - end - - def self.scm_version_string - ret = "" - begin - ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class - rescue Exception => e - logger.error "scm: error during get version string: #{e.message}" - end - ret - end - - def self.scm_available - ret = false - begin - ret = self.scm_adapter_class.client_available if self.scm_adapter_class - rescue Exception => e - logger.error "scm: error during get scm available: #{e.message}" - end - ret - end - - def set_as_default? - new_record? && project && !Repository.first(:conditions => {:project_id => project.id}) - end - - protected - - def check_default - if !is_default? && set_as_default? - self.is_default = true - end - if is_default? && is_default_changed? - Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id]) - end - end - - def load_entries_changesets(entries) - if entries - entries.each do |entry| - if entry.lastrev && entry.lastrev.identifier - entry.changeset = find_changeset_by_name(entry.lastrev.identifier) - end - end - end - end - - private - - # Deletes repository data - def clear_changesets - cs = Changeset.table_name - ch = Change.table_name - ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}" - cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}" - - connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") - connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") - connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") - connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}") - clear_extra_info_of_changesets - end - - def clear_extra_info_of_changesets - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/01/01eefd173bf6e8d083047e854c1bec1880bf6c4e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01eefd173bf6e8d083047e854c1bec1880bf6c4e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,950 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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. + joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt"). + where("child.id IN (?)", ids). + order("#{Project.table_name}.lft ASC"). + uniq. + all + 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?} + self.class.sort_issues!(issues) + if issues + render_issues(issues, options) + return if abort? + end + versions = project_versions(project) + self.class.sort_versions!(versions) + 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 + self.class.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 + + def self.sort_issues!(issues) + issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)} + end + + def self.sort_issue_logic(issue) + julian_date = Date.new() + ancesters_start_date = [] + current_issue = issue + begin + ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id]) + current_issue = current_issue.parent + end while (current_issue) + ancesters_start_date + end + + def self.sort_versions!(versions) + versions.sort! + 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 d98d22a98252 -r fb9a13467253 .svn/pristine/02/020539cf91037b37b7739ed19a56c058529a8860.svn-base --- a/.svn/pristine/02/020539cf91037b37b7739ed19a56c058529a8860.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 WorkflowPermission < WorkflowRule - validates_inclusion_of :rule, :in => %w(readonly required) - validate :validate_field_name - - # Replaces the workflow permissions for the given tracker and role - # - # Example: - # WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}} - def self.replace_permissions(tracker, role, permissions) - destroy_all(:tracker_id => tracker.id, :role_id => role.id) - - permissions.each { |field, rule_by_status_id| - rule_by_status_id.each { |status_id, rule| - if rule.present? - WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule) - end - } - } - end - - protected - - def validate_field_name - unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/) - errors.add :field_name, :invalid - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/02/0235738b45d2757cc6462ce5ba2c1e87548fc84e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/0235738b45d2757cc6462ce5ba2c1e87548fc84e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .svn/pristine/02/02795f92f4fa997a818d2a8cff87758b237ae0bf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/02795f92f4fa997a818d2a8cff87758b237ae0bf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,59 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ThemesTest < ActiveSupport::TestCase + + def test_themes + themes = Redmine::Themes.themes + assert_kind_of Array, themes + assert_kind_of Redmine::Themes::Theme, themes.first + end + + def test_rescan + Redmine::Themes.themes.pop + + assert_difference 'Redmine::Themes.themes.size' do + Redmine::Themes.rescan + end + end + + def test_theme_loaded + theme = Redmine::Themes.themes.last + + assert_equal theme, Redmine::Themes.theme(theme.id) + end + + def test_theme_loaded_without_rescan + theme = Redmine::Themes.themes.last + + assert_equal theme, Redmine::Themes.theme(theme.id, :rescan => false) + end + + def test_theme_not_loaded + theme = Redmine::Themes.themes.pop + + assert_equal theme, Redmine::Themes.theme(theme.id) + end + + def test_theme_not_loaded_without_rescan + theme = Redmine::Themes.themes.pop + + assert_nil Redmine::Themes.theme(theme.id, :rescan => false) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/02/0282f7d2cfc4f0e2579792f126b478789007d665.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/0282f7d2cfc4f0e2579792f126b478789007d665.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,110 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::IssueCategoriesTest < Redmine::ApiTest::Base + fixtures :projects, :users, :issue_categories, :issues, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + end + + test "GET /projects/:project_id/issue_categories.xml should return the issue categories" do + get '/projects/1/issue_categories.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_categories', + :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}} + end + + test "GET /issue_categories/:id.xml should return the issue category" do + get '/issue_categories/2.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_category', + :child => {:tag => 'id', :content => '2'} + end + + test "POST /projects/:project_id/issue_categories.xml should return create issue category" do + assert_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + category = IssueCategory.first(:order => 'id DESC') + assert_equal 'API', category.name + assert_equal 1, category.project_id + end + + test "POST /projects/:project_id/issue_categories.xml with invalid parameters should return errors" do + assert_no_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + + test "PUT /issue_categories/:id.xml with valid parameters should update the issue category" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'API Update', IssueCategory.find(2).name + end + + test "PUT /issue_categories/:id.xml with invalid parameters should return errors" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + + test "DELETE /issue_categories/:id.xml should destroy the issue category" do + assert_difference 'IssueCategory.count', -1 do + delete '/issue_categories/1.xml', {}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end + + test "DELETE /issue_categories/:id.xml should reassign issues with :reassign_to_id param" do + issue_count = Issue.where(:category_id => 1).count + assert issue_count > 0 + + assert_difference 'IssueCategory.count', -1 do + assert_difference 'Issue.where(:category_id => 2).count', 3 do + delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith') + end + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/02/02ceadd1b8d78a178efab59eb94d40a87ff683f1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/02ceadd1b8d78a178efab59eb94d40a87ff683f1.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,149 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::AttachmentsTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :attachments + + def setup + Setting.rest_api_enabled = '1' + set_fixtures_attachments_directory + end + + def teardown + set_tmp_attachments_directory + end + + test "GET /attachments/:id.xml should return the attachment" do + get '/attachments/7.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'attachment', + :child => { + :tag => 'id', + :content => '7', + :sibling => { + :tag => 'filename', + :content => 'archive.zip', + :sibling => { + :tag => 'content_url', + :content => 'http://www.example.com/attachments/download/7/archive.zip' + } + } + } + end + + test "GET /attachments/:id.xml should deny access without credentials" do + get '/attachments/7.xml' + assert_response 401 + set_tmp_attachments_directory + end + + test "GET /attachments/download/:id/:filename should return the attachment content" do + get '/attachments/download/7/archive.zip', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + set_tmp_attachments_directory + end + + test "GET /attachments/download/:id/:filename should deny access without credentials" do + get '/attachments/download/7/archive.zip' + assert_response 302 + set_tmp_attachments_directory + end + + test "POST /uploads.xml should return the token" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.xml', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + assert_equal 'application/xml', response.content_type + end + + xml = Hash.from_xml(response.body) + assert_kind_of Hash, xml['upload'] + token = xml['upload']['token'] + assert_not_nil token + + attachment = Attachment.order('id DESC').first + assert_equal token, attachment.token + assert_nil attachment.container + assert_equal 2, attachment.author_id + assert_equal 'File content'.size, attachment.filesize + assert attachment.content_type.blank? + assert attachment.filename.present? + assert_match /\d+_[0-9a-z]+/, attachment.diskfile + assert File.exist?(attachment.diskfile) + assert_equal 'File content', File.read(attachment.diskfile) + end + + test "POST /uploads.json should return the token" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.json', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + assert_equal 'application/json', response.content_type + end + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json['upload'] + token = json['upload']['token'] + assert_not_nil token + + attachment = Attachment.order('id DESC').first + assert_equal token, attachment.token + end + + test "POST /uploads.xml should accept :filename param as the attachment filename" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.xml?filename=test.txt', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + + attachment = Attachment.order('id DESC').first + assert_equal 'test.txt', attachment.filename + assert_match /_test\.txt$/, attachment.diskfile + end + + test "POST /uploads.xml should not accept other content types" do + set_tmp_attachments_directory + assert_no_difference 'Attachment.count' do + post '/uploads.xml', 'PNG DATA', {"CONTENT_TYPE" => 'image/png'}.merge(credentials('jsmith')) + assert_response 406 + end + end + + test "POST /uploads.xml should return errors if file is too big" do + set_tmp_attachments_directory + with_settings :attachment_max_size => 1 do + assert_no_difference 'Attachment.count' do + post '/uploads.xml', ('x' * 2048), {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response 422 + assert_tag 'error', :content => /exceeds the maximum allowed file size/ + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/03186ac93c7f57779f7557150cdd51f4c66c78c8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03186ac93c7f57779f7557150cdd51f4c66c78c8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,34 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::MenuManagerTest < ActiveSupport::TestCase + def test_map_should_yield_a_mapper + assert_difference 'Redmine::MenuManager.items(:project_menu).size' do + Redmine::MenuManager.map :project_menu do |mapper| + assert_kind_of Redmine::MenuManager::Mapper, mapper + mapper.push :new_item, '/' + end + end + end + + def test_items_should_return_menu_items + items = Redmine::MenuManager.items(:project_menu) + assert_kind_of Redmine::MenuManager::MenuNode, items.first + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/0327b87f817dd6e7b29c9fc60cf3afcc96b39ebb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/0327b87f817dd6e7b29c9fc60cf3afcc96b39ebb.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,32 @@ +

<%=l(:label_enumerations)%>

+ +<% Enumeration.get_subclasses.each do |klass| %> +

<%= l(klass::OptionName) %>

+ +<% enumerations = klass.shared %> +<% if enumerations.any? %> + + + + + + + + +<% enumerations.each do |enumeration| %> + + + + + + + +<% end %> +
<%= 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) %>
+<% reset_cycle %> +<% end %> + +

<%= link_to l(:label_enumeration_new), new_enumeration_path(:type => klass.name) %>

+<% end %> + +<% html_title(l(:label_enumerations)) -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/0335a712dd5fb2c44f8cb6daaf182ac72aee4c7f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/0335a712dd5fb2c44f8cb6daaf182ac72aee4c7f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,287 @@ + + + +RedmineWikiFormatting + + + + + +

Wiki æ ¼å¼è¨­å®š

+ +

連çµ

+ +

Redmine 連çµ

+ +

在任何å¯ä»¥ä½¿ç”¨ Wiki æ ¼å¼è¨­å®šçš„地方, Redmine 都å…許在資æºï¼ˆå•題ã€è®Šæ›´é›†ã€ Wiki é é¢...)間建立超連çµã€‚

+
    +
  • 連çµè‡³ä¸€å€‹å•題: #124 (若該å•é¡Œå·²ç¶“çµæŸï¼Œå‰‡ä½¿ç”¨åˆªé™¤ç·šé¡¯ç¤ºé€£çµï¼š #124)
  • +
  • 連çµè‡³ä¸€å€‹å•題的筆記: #124-6, or #124#note-6
  • +
+ +

Wiki 連çµï¼š

+ +
    +
  • [[Guide]] 顯示一個é é¢å稱為 'Guide' 的連çµï¼š Guide
  • +
  • [[Guide#further-reading]] 會連çµè‡³ä¸€å€‹ "further-reading" çš„ HTML 錨定 (anchor) 。æ¯å€‹æ¨™é¡Œæ–‡å­—都會被自動指定一個 HTML 錨定,以便您å¯ä»¥ç”¨ä¾†é€£çµå®ƒå€‘: Guide
  • +
  • [[Guide|User manual]] 使用ä¸åŒçš„æ–‡å­—來顯示一個é é¢å稱為 'Guide' 的連çµï¼š User manual
  • +
+ +

您也å¯ä»¥é€£çµè‡³å…¶ä»–專案的 Wiki é é¢ï¼š

+ +
    +
  • [[sandbox:some page]] 顯示一個 Sanbox wiki 中é é¢å稱為 'Some page' 的連çµ
  • +
  • [[sandbox:]] 顯示 Sandbox wiki 首é é é¢çš„連çµ
  • +
+ +

ç•¶é é¢ä¸å­˜åœ¨çš„æ™‚候, Wiki é€£çµæœƒä»¥ç´…色的方å¼é¡¯ç¤ºï¼Œä¾‹å¦‚: Nonexistent page.

+ +

連çµè‡³å…¶ä»–資æºï¼š

+ +
    +
  • 文件: +
      +
    • document#17 (連çµåˆ°ç·¨è™Ÿç‚º 17 的文件)
    • +
    • document:Greetings (連çµè‡³æ–‡ä»¶æ¨™é¡Œç‚º "Greetings" 的文件)
    • +
    • document:"Some document" (文件標題包å«ç©ºç™½å­—元時å¯ä»¥ä½¿ç”¨é›™å¼•號來標示)
    • +
    • sandbox:document:"Some document" (連çµè‡³å¦å¤–一個 "sandbox" 專案中,文件標題為 "Some document" 的文件)
    • +
  • +
+ +
    +
  • 版本: +
      +
    • version#3 (連çµè‡³ç·¨è™Ÿç‚º 3 的版本)
    • +
    • version:1.0.0 (連çµè‡³å稱為 "1.0.0" 的版本)
    • +
    • version:"1.0 beta 2" (版本å稱包å«ç©ºç™½å­—元時å¯ä»¥ä½¿ç”¨é›™å¼•號來標示)
    • +
    • sandbox:version:1.0.0 (連çµè‡³ "sandbox" 專案中,å稱為 "1.0.0" 的版本)
    • +
  • +
+ +
    +
  • 附加檔案: +
      +
    • attachment:file.zip (連çµè‡³ç›®å‰ç‰©ä»¶ä¸­ï¼Œå稱為 file.zip 的附加檔案)
    • +
    • ç›®å‰åƒ…æä¾›åƒè€ƒåˆ°ç›®å‰ç‰©ä»¶ä¸­çš„附加檔案 (è‹¥æ‚¨æ­£ä½æ–¼ä¸€å€‹å•題中,僅å¯åƒè€ƒä½æ–¼æ­¤å•題中之附加檔案)
    • +
  • +
+ +
    +
  • 變更集: +
      +
    • r758 (連çµè‡³ä¸€å€‹è®Šæ›´é›†)
    • +
    • commit:c6f4d0fd (連çµè‡³ä¸€å€‹ä½¿ç”¨éžæ•¸å­—雜湊的變更集)
    • +
    • svn1|r758 (連çµè‡³æŒ‡å®šå„²å­˜æ©Ÿåˆ¶ä¸­ä¹‹è®Šæ›´é›†ï¼Œç”¨æ–¼å°ˆæ¡ˆä½¿ç”¨å¤šå€‹å„²å­˜æ©Ÿåˆ¶æ™‚之情æ³)
    • +
    • commit:hg|c6f4d0fd (連çµè‡³æŒ‡å®šå„²å­˜æ©Ÿåˆ¶ä¸­ï¼Œä½¿ç”¨éžæ•¸å­—雜湊的變更集)
    • +
    • sandbox:r758 (連çµè‡³å…¶ä»–專案的變更集)
    • +
    • sandbox:commit:c6f4d0fd (連çµè‡³å…¶ä»–å°ˆæ¡ˆä¸­ï¼Œä½¿ç”¨éžæ•¸å­—雜湊的變更集)
    • +
  • +
+ +
    +
  • 儲存機制中之檔案: +
      +
    • source:some/file (連çµè‡³å°ˆæ¡ˆå„²å­˜æ©Ÿåˆ¶ä¸­ï¼Œä½æ–¼ /some/file 的檔案)
    • +
    • source:some/file@52 (連çµè‡³æ­¤æª”案的 52 版次)
    • +
    • source:some/file#L120 (連çµè‡³æ­¤æª”案的第 120 行)
    • +
    • source:some/file@52#L120 (連çµè‡³æ­¤æª”案的 52 版刺中之第 120 行)
    • +
    • source:"some file@52#L120" (ç•¶ URL 中包å«ç©ºç™½å­—元時,使用雙引號來標示)
    • +
    • export:some/file (強制下載此檔案)
    • +
    • source:svn1|some/file (連çµè‡³æŒ‡å®šå„²å­˜æ©Ÿåˆ¶ä¸­çš„æ­¤æª”案,用於專案使用多個儲存機制時之情æ³)
    • +
    • sandbox:source:some/file (連çµè‡³ "sandbox" å°ˆæ¡ˆçš„å„²å­˜æ©Ÿåˆ¶ä¸­ï¼Œä½æ–¼ /some/file 的檔案)
    • +
    • sandbox:export:some/file (強迫下載該檔案)
    • +
  • +
+ +
    +
  • 論壇訊æ¯ï¼š +
      +
    • message#1218 (連çµè‡³ç·¨è™Ÿ 1218 的訊æ¯)
    • +
  • +
+ +
    +
  • 專案: +
      +
    • project#3 (連çµè‡³ç·¨è™Ÿç‚º 3 的專案)
    • +
    • project:someproject (連çµè‡³å稱為 "someproject" 的專案)
    • +
  • +
+ + +

逸出字元:

+ +
    +
  • 您å¯ä»¥åœ¨æ–‡å­—çš„å‰é¢åŠ ä¸Šé©šå˜†è™Ÿ (!) 來é¿å…è©²æ–‡å­—è¢«å‰–æžæˆ Redmine 連çµ
  • +
+ + +

外部連çµ

+ +

HTTP URLs 與電å­éƒµä»¶åœ°å€æœƒè‡ªå‹•è¢«è½‰æ›æˆå¯è¢«é»žæ“Šçš„連çµï¼š

+ +
+http://www.redmine.org, someone@foo.bar
+
+ +

顯示為: http://www.redmine.org,

+ +

若您想è¦é¡¯ç¤ºæŒ‡å®šçš„æ–‡å­—而éžè©² URL ,您å¯ä»¥ä½¿ç”¨ä¸‹åˆ—標準的 textile 語法:

+ +
+"Redmine web site":http://www.redmine.org
+
+ +

顯示為: Redmine web site

+ + +

文字格å¼è¨­å®š

+ + +

å°æ–¼è«¸å¦‚標題ã€ç²—é«”ã€è¡¨æ ¼ã€æ¸…單等項目, Redmine 支æ´ä½¿ç”¨ Textile 語法。å¯åƒè€ƒ http://en.wikipedia.org/wiki/Textile_(markup_language) 中關於使用這些格å¼åŒ–功能的說明資訊。 下é¢åŒ…å«äº†ä¸€äº›ä½¿ç”¨ç¯„例,但格å¼åŒ–引擎的處ç†èƒ½åŠ›é å¤šæ–¼é€™äº›ç°¡å–®çš„使用範例。

+ +

字型樣å¼

+ +
+* *ç²—é«”*
+* _斜體_
+* _*粗斜體*_
+* +底線+
+* -刪除線-
+
+ +

顯示為:

+ +
    +
  • ç²—é«”
  • +
  • 斜體
  • +
  • 粗斜體
  • +
  • 底線
  • +
  • 刪除線
  • +
+ +

內置圖åƒ

+ +
    +
  • !圖åƒ_url! é¡¯ç¤ºä¸€å€‹ä½æ–¼ 圖åƒ_url ä½å€çš„圖åƒ(textile 語法)
  • +
  • !>image_url! å³å´æµ®å‹•圖åƒ
  • +
  • 若您附加了一個圖åƒåˆ° Wiki é é¢ä¸­ï¼Œå¯ä»¥ä½¿ç”¨ä»–的檔案å稱來顯示æˆå…§ç½®åœ–åƒï¼š !attached_image.png!
  • +
+ +

標題

+ +
+h1. 標題
+h2. 次標題
+h3. 次次標題
+
+ +

Redmine 為æ¯ä¸€ç¨®æ¨™é¡ŒæŒ‡å®šä¸€å€‹ HTML 錨定 (anchor) ,因此您å¯ä½¿ç”¨ "#Heading" 〠"#Subheading" 等方å¼é€£çµè‡³é€™äº›æ¨™é¡Œã€‚

+ + +

段è½

+ +
+p>. é å³å°é½Š
+p=. 置中å°é½Š
+
+ +

這是一個置中å°é½Šçš„æ®µè½ã€‚

+ + +

引用文字

+ +

使用 bq. 開始一個引文的段è½

+ +
+bq. Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.
+To go live, all you need to add is a database and a web server.
+
+ +

顯示為:

+ +
+

Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.
To go live, all you need to add is a database and a web server.

+
+ + +

目錄

+ +
+{{toc}} => é å·¦å°é½Šç›®éŒ„
+{{>toc}} => é å³å°é½Šç›®éŒ„
+
+ +

水平線

+ +
+---
+
+ +

巨集

+ +

Redmine 內建下列巨集:

+ +

hello_world

範例巨集。

include

引入一個 wiki é é¢ã€‚例å­ï¼š

+ +
{{include(Foo)}}
macro_list

顯示所有å¯ç”¨å·¨é›†çš„æ¸…單,若巨集有æä¾›èªªæ˜Žä¹Ÿæœƒä¸€ä½µé¡¯ç¤ºã€‚

+ + +

程å¼ç¢¼é†’ç›®æç¤º

+ +

é è¨­ä½¿ç”¨ CodeRay 作為程å¼ç¢¼é†’ç›®æç¤ºçš„æ©Ÿåˆ¶ï¼Œå®ƒæ˜¯ä¸€å€‹ä½¿ç”¨ Ruby 撰寫的語法醒目æç¤ºå‡½å¼åº«ã€‚å®ƒç›®å‰æ”¯æ´ c 〠cpp 〠css 〠delphi 〠groovy 〠html 〠java 〠javascript 〠json 〠php 〠python 〠rhtml 〠ruby 〠scheme 〠sql 〠xml 與 yaml 等程å¼èªžè¨€ã€‚

+ +

您å¯ä»¥ä½¿ç”¨ä¸‹åˆ—語法,在 Wiki é é¢ä¸­å°‡ç¨‹å¼ç¢¼æ¨™ç¤ºç‚ºé†’ç›®æç¤ºï¼š

+ +
+<pre><code class="ruby">
+  將程å¼ç¢¼æ”¾åœ¨é€™è£¡ã€‚
+</code></pre>
+
+ +

例å­ï¼š

+ +
 1 # The Greeter class
+ 2 class Greeter
+ 3   def initialize(name)
+ 4     @name = name.capitalize
+ 5   end
+ 6
+ 7   def salute
+ 8     puts "Hello #{@name}!"
+ 9   end
+10 end
+
+ + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/03387de6b294853cc867f560276231ad9e9e4136.svn-base --- a/.svn/pristine/03/03387de6b294853cc867f560276231ad9e9e4136.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -== Redmine upgrade - -Redmine - project management software -Copyright (C) 2006-2012 Jean-Philippe Lang -http://www.redmine.org/ - - -== Upgrading - -1. Uncompress the program archive in a new directory - -2. Copy your database settings (RAILS_ROOT/config/database.yml) - and your configuration file (RAILS_ROOT/config/configuration.yml) - into the new config directory - Note: before Redmine 1.2, SMTP configuration was stored in - config/email.yml. It should now be stored in config/configuration.yml. - -3. Copy the RAILS_ROOT/files directory content into your new installation - This directory contains all the attached files. - -4. Copy the folders of the installed plugins and themes into new installation - Plugins must be stored in the [redmine_root]/plugins directory - Themes must be stored in the [redmine_root]/public/themes directory - - WARNING: plugins from your previous Redmine version may not be compatible - with the Redmine version you're upgrading to. - -5. Install the required gems by running: - bundle install --without development test - - If ImageMagick is not installed on your system, you should skip the installation - of the rmagick gem using: - bundle install --without development test rmagick - -6. Generate a session store secret - - Redmine stores session data in cookies by default, which requires - a secret to be generated. Under the new application directory run: - rake generate_secret_token - - DO NOT REPLACE OR EDIT ANY OTHER FILES. - -7. Migrate your database - - If you are upgrading to Rails 2.3.14 as part of this migration, you - need to upgrade the plugin migrations before running the plugin migrations - using: - rake db:migrate:upgrade_plugin_migrations RAILS_ENV="production" - - Please make a backup before doing this! Under the new application - directory run: - rake db:migrate RAILS_ENV="production" - - If you have installed any plugins, you should also run their database - migrations using: - rake db:migrate_plugins RAILS_ENV="production" - -8. Clear the cache and the existing sessions by running: - rake tmp:cache:clear - rake tmp:sessions:clear - -9. Restart the application server (e.g. mongrel, thin, passenger) - -10. Finally go to "Administration -> Roles & permissions" to check/set permissions - for new features, if any - -== References - -* http://www.redmine.org/wiki/redmine/RedmineUpgrade diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/038dcd875902d442a622bf1915b8e83469f95936.svn-base --- a/.svn/pristine/03/038dcd875902d442a622bf1915b8e83469f95936.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,177 +0,0 @@ -require File.dirname(__FILE__) + '/helper' -require File.dirname(__FILE__) + '/../init' - -class PaginationTest < ActiveRecordTestCase - fixtures :topics, :replies, :developers, :projects, :developers_projects - - class PaginationController < ActionController::Base - if respond_to? :view_paths= - self.view_paths = [ "#{File.dirname(__FILE__)}/../fixtures/" ] - else - self.template_root = [ "#{File.dirname(__FILE__)}/../fixtures/" ] - end - - def simple_paginate - @topic_pages, @topics = paginate(:topics) - render :nothing => true - end - - def paginate_with_per_page - @topic_pages, @topics = paginate(:topics, :per_page => 1) - render :nothing => true - end - - def paginate_with_order - @topic_pages, @topics = paginate(:topics, :order => 'created_at asc') - render :nothing => true - end - - def paginate_with_order_by - @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc') - render :nothing => true - end - - def paginate_with_include_and_order - @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc') - render :nothing => true - end - - def paginate_with_conditions - @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago]) - render :nothing => true - end - - def paginate_with_class_name - @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR") - render :nothing => true - end - - def paginate_with_singular_name - @developer_pages, @developers = paginate() - render :nothing => true - end - - def paginate_with_joins - @developer_pages, @developers = paginate(:developers, - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1') - render :nothing => true - end - - def paginate_with_join - @developer_pages, @developers = paginate(:developers, - :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1') - render :nothing => true - end - - def paginate_with_join_and_count - @developer_pages, @developers = paginate(:developers, - :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id', - :conditions => 'project_id=1', - :count => "d.id") - render :nothing => true - end - - def paginate_with_join_and_group - @developer_pages, @developers = paginate(:developers, - :join => 'INNER JOIN developers_projects ON developers.id = developers_projects.developer_id', - :group => 'developers.id') - render :nothing => true - end - - def rescue_errors(e) raise e end - - def rescue_action(e) raise end - - end - - def setup - @controller = PaginationController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - super - end - - # Single Action Pagination Tests - - def test_simple_paginate - get :simple_paginate - assert_equal 1, assigns(:topic_pages).page_count - assert_equal 3, assigns(:topics).size - end - - def test_paginate_with_per_page - get :paginate_with_per_page - assert_equal 1, assigns(:topics).size - assert_equal 3, assigns(:topic_pages).page_count - end - - def test_paginate_with_order - get :paginate_with_order - expected = [topics(:futurama), - topics(:harvey_birdman), - topics(:rails)] - assert_equal expected, assigns(:topics) - assert_equal 1, assigns(:topic_pages).page_count - end - - def test_paginate_with_order_by - get :paginate_with_order - expected = assigns(:topics) - get :paginate_with_order_by - assert_equal expected, assigns(:topics) - assert_equal 1, assigns(:topic_pages).page_count - end - - def test_paginate_with_conditions - get :paginate_with_conditions - expected = [topics(:rails)] - assert_equal expected, assigns(:topics) - assert_equal 1, assigns(:topic_pages).page_count - end - - def test_paginate_with_class_name - get :paginate_with_class_name - - assert assigns(:developers).size > 0 - assert_equal DeVeLoPeR, assigns(:developers).first.class - end - - def test_paginate_with_joins - get :paginate_with_joins - assert_equal 2, assigns(:developers).size - developer_names = assigns(:developers).map { |d| d.name } - assert developer_names.include?('David') - assert developer_names.include?('Jamis') - end - - def test_paginate_with_join_and_conditions - get :paginate_with_joins - expected = assigns(:developers) - get :paginate_with_join - assert_equal expected, assigns(:developers) - end - - def test_paginate_with_join_and_count - get :paginate_with_joins - expected = assigns(:developers) - get :paginate_with_join_and_count - assert_equal expected, assigns(:developers) - end - - def test_paginate_with_include_and_order - get :paginate_with_include_and_order - expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10) - assert_equal expected, assigns(:topics) - end - - def test_paginate_with_join_and_group - get :paginate_with_join_and_group - assert_equal 2, assigns(:developers).size - assert_equal 2, assigns(:developer_pages).item_count - developer_names = assigns(:developers).map { |d| d.name } - assert developer_names.include?('David') - assert developer_names.include?('Jamis') - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/03afb00250a194ff34ffdc8b11c9b63195093f99.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03afb00250a194ff34ffdc8b11c9b63195093f99.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,130 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RepositoryDarcsTest < ActiveSupport::TestCase + fixtures :projects + + include Redmine::I18n + + REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s + NUM_REV = 6 + + def setup + @project = Project.find(3) + @repository = Repository::Darcs.create( + :project => @project, + :url => REPOSITORY_PATH, + :log_encoding => 'UTF-8' + ) + assert @repository + end + + def test_blank_path_to_repository_error_message + set_language_if_valid 'en' + repo = Repository::Darcs.new( + :project => @project, + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include "Path to repository can't be blank", + repo.errors.full_messages + end + + def test_blank_path_to_repository_error_message_fr + set_language_if_valid 'fr' + str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + repo = Repository::Darcs.new( + :project => @project, + :url => "", + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include str, repo.errors.full_messages + end + + if File.directory?(REPOSITORY_PATH) + def test_fetch_changesets_from_scratch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + + assert_equal NUM_REV, @repository.changesets.count + assert_equal 13, @repository.filechanges.count + assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments + end + + def test_fetch_changesets_incremental + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + # Remove changesets with revision > 3 + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 3} + @project.reload + @repository.reload + assert_equal 3, @repository.changesets.count + + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + end + + def test_entries + entries = @repository.entries + assert_kind_of Redmine::Scm::Adapters::Entries, entries + end + + def test_entries_invalid_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_nil @repository.entries('', '123') + end + + def test_deleted_files_should_not_be_listed + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + entries = @repository.entries('sources') + assert entries.detect {|e| e.name == 'watchers_controller.rb'} + assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'} + end + + def test_cat + if @repository.scm.supports_cat? + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + cat = @repository.cat("sources/welcome_controller.rb", 2) + assert_not_nil cat + assert cat.include?('class WelcomeController < ApplicationController') + end + end + else + puts "Darcs test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/03da074b61ddfe4ad348c236abb94ecdb763073e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03da074b61ddfe4ad348c236abb94ecdb763073e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,111 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 NewsController < ApplicationController + default_search_scope :news + model_object News + before_filter :find_model_object, :except => [:new, :create, :index] + before_filter :find_project_from_association, :except => [:new, :create, :index] + before_filter :find_project_by_project_id, :only => [:new, :create] + before_filter :authorize, :except => [:index] + before_filter :find_optional_project, :only => :index + accept_rss_auth :index + accept_api_auth :index + + helper :watchers + helper :attachments + + def index + case params[:format] + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = 10 + end + + scope = @project ? @project.news.visible : News.visible + + @news_count = scope.count + @news_pages = Paginator.new @news_count, @limit, params['page'] + @offset ||= @news_pages.offset + @newss = scope.all(:include => [:author, :project], + :order => "#{News.table_name}.created_on DESC", + :offset => @offset, + :limit => @limit) + + respond_to do |format| + format.html { + @news = News.new # for adding news inline + render :layout => false if request.xhr? + } + format.api + format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } + end + end + + def show + @comments = @news.comments + @comments.reverse! if User.current.wants_comments_in_reverse_order? + end + + def new + @news = News.new(:project => @project, :author => User.current) + end + + def create + @news = News.new(:project => @project, :author => User.current) + @news.safe_attributes = params[:news] + @news.save_attachments(params[:attachments]) + if @news.save + render_attachment_warning_if_needed(@news) + flash[:notice] = l(:notice_successful_create) + redirect_to project_news_index_path(@project) + else + render :action => 'new' + end + end + + def edit + end + + def update + @news.safe_attributes = params[:news] + @news.save_attachments(params[:attachments]) + if @news.save + render_attachment_warning_if_needed(@news) + flash[:notice] = l(:notice_successful_update) + redirect_to news_path(@news) + else + render :action => 'edit' + end + end + + def destroy + @news.destroy + redirect_to project_news_index_path(@project) + end + + private + + def find_optional_project + return true unless params[:project_id] + @project = Project.find(params[:project_id]) + authorize + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/03/03dacc5eb19ef8f9c13510c3c9db9d59186398e2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03dacc5eb19ef8f9c13510c3c9db9d59186398e2.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,26 @@ +class StoreRelationTypeInJournalDetails < ActiveRecord::Migration + + MAPPING = { + "label_relates_to" => "relates", + "label_duplicates" => "duplicates", + "label_duplicated_by" => "duplicated", + "label_blocks" => "blocks", + "label_blocked_by" => "blocked", + "label_precedes" => "precedes", + "label_follows" => "follows", + "label_copied_to" => "copied_to", + "label_copied_from" => "copied_from" + } + + def up + StoreRelationTypeInJournalDetails::MAPPING.each do |prop_key, replacement| + JournalDetail.where(:property => 'relation', :prop_key => prop_key).update_all(:prop_key => replacement) + end + end + + def down + StoreRelationTypeInJournalDetails::MAPPING.each do |prop_key, replacement| + JournalDetail.where(:property => 'relation', :prop_key => replacement).update_all(:prop_key => prop_key) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/0406b4818e714c1d680ae173e23dbc29be7a3556.svn-base --- a/.svn/pristine/04/0406b4818e714c1d680ae173e23dbc29be7a3556.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1100 +0,0 @@ -# Polish translations for Ruby on Rails -# by Jacek Becela (jacek.becela@gmail.com, http://github.com/ncr) -# by Krzysztof Podejma (kpodejma@customprojects.pl, http://www.customprojects.pl) - -pl: - number: - format: - separator: "," - delimiter: " " - precision: 2 - currency: - format: - format: "%n %u" - unit: "PLN" - percentage: - format: - delimiter: "" - precision: - format: - delimiter: "" - human: - format: - delimiter: "" - precision: 3 - storage_units: - format: "%n %u" - units: - byte: - one: "B" - other: "B" - kb: "KB" - mb: "MB" - gb: "GB" - tb: "TB" - - direction: ltr - date: - formats: - default: "%Y-%m-%d" - short: "%d %b" - long: "%d %B %Y" - - day_names: [Niedziela, PoniedziaÅ‚ek, Wtorek, Åšroda, Czwartek, PiÄ…tek, Sobota] - abbr_day_names: [nie, pon, wto, Å›ro, czw, pia, sob] - - month_names: [~, StyczeÅ„, Luty, Marzec, KwiecieÅ„, Maj, Czerwiec, Lipiec, SierpieÅ„, WrzesieÅ„, Październik, Listopad, GrudzieÅ„] - abbr_month_names: [~, sty, lut, mar, kwi, maj, cze, lip, sie, wrz, paź, lis, gru] - order: - - :year - - :month - - :day - - time: - formats: - default: "%a, %d %b %Y, %H:%M:%S %z" - time: "%H:%M" - short: "%d %b, %H:%M" - long: "%d %B %Y, %H:%M" - am: "przed poÅ‚udniem" - pm: "po poÅ‚udniu" - - datetime: - distance_in_words: - half_a_minute: "pół minuty" - less_than_x_seconds: - one: "mniej niż sekundÄ™" - few: "mniej niż %{count} sekundy" - other: "mniej niż %{count} sekund" - x_seconds: - one: "sekundÄ™" - few: "%{count} sekundy" - other: "%{count} sekund" - less_than_x_minutes: - one: "mniej niż minutÄ™" - few: "mniej niż %{count} minuty" - other: "mniej niż %{count} minut" - x_minutes: - one: "minutÄ™" - few: "%{count} minuty" - other: "%{count} minut" - about_x_hours: - one: "okoÅ‚o godziny" - other: "okoÅ‚o %{count} godzin" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "1 dzieÅ„" - other: "%{count} dni" - about_x_months: - one: "okoÅ‚o miesiÄ…ca" - other: "okoÅ‚o %{count} miesiÄ™cy" - x_months: - one: "1 miesiÄ…c" - few: "%{count} miesiÄ…ce" - other: "%{count} miesiÄ™cy" - about_x_years: - one: "okoÅ‚o roku" - other: "okoÅ‚o %{count} lat" - over_x_years: - one: "ponad rok" - few: "ponad %{count} lata" - other: "ponad %{count} lat" - almost_x_years: - one: "prawie rok" - other: "prawie %{count} lata" - - activerecord: - errors: - template: - header: - one: "%{model} nie zostaÅ‚ zachowany z powodu jednego błędu" - other: "%{model} nie zostaÅ‚ zachowany z powodu %{count} błędów" - body: "Błędy dotyczÄ… nastÄ™pujÄ…cych pól:" - messages: - inclusion: "nie znajduje siÄ™ na liÅ›cie dopuszczalnych wartoÅ›ci" - exclusion: "znajduje siÄ™ na liÅ›cie zabronionych wartoÅ›ci" - invalid: "jest nieprawidÅ‚owe" - confirmation: "nie zgadza siÄ™ z potwierdzeniem" - accepted: "musi być zaakceptowane" - empty: "nie może być puste" - blank: "nie może być puste" - too_long: "jest za dÅ‚ugie (maksymalnie %{count} znaków)" - too_short: "jest za krótkie (minimalnie %{count} znaków)" - wrong_length: "jest nieprawidÅ‚owej dÅ‚ugoÅ›ci (powinna wynosić %{count} znaków)" - taken: "jest już zajÄ™te" - not_a_number: "nie jest liczbÄ…" - greater_than: "musi być wiÄ™ksze niż %{count}" - greater_than_or_equal_to: "musi być wiÄ™ksze lub równe %{count}" - equal_to: "musi być równe %{count}" - less_than: "musi być mniejsze niż %{count}" - less_than_or_equal_to: "musi być mniejsze lub równe %{count}" - odd: "musi być nieparzyste" - even: "musi być parzyste" - greater_than_start_date: "musi być wiÄ™ksze niż poczÄ…tkowa data" - not_same_project: "nie należy do tego samego projektu" - circular_dependency: "Ta relacja może wytworzyć koÅ‚owÄ… zależność" - cant_link_an_issue_with_a_descendant: "Zagadnienie nie może zostać powiÄ…zane z jednym z wÅ‚asnych podzagadnieÅ„" - - support: - array: - sentence_connector: "i" - skip_last_comma: true - - # Keep this line in order to avoid problems with Windows Notepad UTF-8 EF-BB-BFidea... - # Best regards from Lublin@Poland :-) - # PL translation by Mariusz@Olejnik.net, - actionview_instancetag_blank_option: ProszÄ™ wybierz - - button_activate: Aktywuj - button_add: Dodaj - button_annotate: Adnotuj - button_apply: Ustaw - button_archive: Archiwizuj - button_back: Wstecz - button_cancel: Anuluj - button_change: ZmieÅ„ - button_change_password: ZmieÅ„ hasÅ‚o - button_check_all: Zaznacz wszystko - button_clear: Wyczyść - button_configure: Konfiguruj - button_copy: Kopia - button_create: Stwórz - button_delete: UsuÅ„ - button_download: Pobierz - button_edit: Edytuj - button_list: Lista - button_lock: Zablokuj - button_log_time: Dziennik - button_login: Login - button_move: PrzenieÅ› - button_quote: Cytuj - button_rename: ZmieÅ„ nazwÄ™ - button_reply: Odpowiedz - button_reset: Resetuj - button_rollback: Przywróć do tej wersji - button_save: Zapisz - button_sort: Sortuj - button_submit: WyÅ›lij - button_test: Testuj - button_unarchive: Przywróć z archiwum - button_uncheck_all: Odznacz wszystko - button_unlock: Odblokuj - button_unwatch: Nie obserwuj - button_update: Uaktualnij - button_view: Pokaż - button_watch: Obserwuj - default_activity_design: Projektowanie - default_activity_development: Rozwój - default_doc_category_tech: Dokumentacja techniczna - default_doc_category_user: Dokumentacja użytkownika - default_issue_status_in_progress: W Toku - default_issue_status_closed: ZamkniÄ™ty - default_issue_status_feedback: Odpowiedź - default_issue_status_new: Nowy - default_issue_status_rejected: Odrzucony - default_issue_status_resolved: RozwiÄ…zany - default_priority_high: Wysoki - default_priority_immediate: Natychmiastowy - default_priority_low: Niski - default_priority_normal: Normalny - default_priority_urgent: Pilny - default_role_developer: Programista - default_role_manager: Kierownik - default_role_reporter: Wprowadzajacy - default_tracker_bug: Błąd - default_tracker_feature: Zadanie - default_tracker_support: Wsparcie - enumeration_activities: DziaÅ‚ania (Å›ledzenie czasu) - enumeration_doc_categories: Kategorie dokumentów - enumeration_issue_priorities: Priorytety zagadnieÅ„ - error_can_t_load_default_data: "DomyÅ›lna konfiguracja nie może być zaÅ‚adowana: %{value}" - error_issue_not_found_in_project: 'Zaganienie nie zostaÅ‚o znalezione lub nie należy do tego projektu' - error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji." - error_scm_command_failed: "WystÄ…piÅ‚ błąd przy próbie dostÄ™pu do repozytorium: %{value}" - error_scm_not_found: "Obiekt lub wersja nie zostaÅ‚y znalezione w repozytorium." - field_account: Konto - field_activity: Aktywność - field_admin: Administrator - field_assignable: Zagadnienia mogÄ… być przypisane do tej roli - field_assigned_to: Przydzielony do - field_attr_firstname: ImiÄ™ atrybut - field_attr_lastname: Nazwisko atrybut - field_attr_login: Login atrybut - field_attr_mail: Email atrybut - field_auth_source: Tryb identyfikacji - field_author: Autor - field_base_dn: Base DN - field_category: Kategoria - field_column_names: Nazwy kolumn - field_comments: Komentarz - field_comments_sorting: Pokazuj komentarze - field_created_on: Stworzone - field_default_value: DomyÅ›lny - field_delay: Opóźnienie - field_description: Opis - field_done_ratio: "% Wykonane" - field_downloads: PobraÅ„ - field_due_date: Data oddania - field_effective_date: Data - field_estimated_hours: Szacowany czas - field_field_format: Format - field_filename: Plik - field_filesize: Rozmiar - field_firstname: ImiÄ™ - field_fixed_version: Wersja docelowa - field_hide_mail: Ukryj mój adres email - field_homepage: Strona www - field_host: Host - field_hours: Godzin - field_identifier: Identifikator - field_is_closed: Zagadnienie zamkniÄ™te - field_is_default: DomyÅ›lny status - field_is_filter: Atrybut filtrowania - field_is_for_all: Dla wszystkich projektów - field_is_in_roadmap: Zagadnienie pokazywane na mapie - field_is_public: Publiczny - field_is_required: Wymagane - field_issue: Zagadnienie - field_issue_to: PowiÄ…zania zagadnienia - field_language: JÄ™zyk - field_last_login_on: Ostatnie połączenie - field_lastname: Nazwisko - field_login: Login - field_mail: Email - field_mail_notification: Powiadomienia Email - field_max_length: Maksymalna dÅ‚ugość - field_min_length: Minimalna dÅ‚ugość - field_name: Nazwa - field_new_password: Nowe hasÅ‚o - field_notes: Notatki - field_onthefly: Tworzenie użytkownika w locie - field_parent: Nadprojekt - field_parent_title: Strona rodzica - field_password: HasÅ‚o - field_password_confirmation: Potwierdzenie - field_port: Port - field_possible_values: Możliwe wartoÅ›ci - field_priority: Priorytet - field_project: Projekt - field_redirect_existing_links: Przekierowanie istniejÄ…cych odnoÅ›ników - field_regexp: Wyrażenie regularne - field_role: Rola - field_searchable: Przeszukiwalne - field_spent_on: Data - field_start_date: Start - field_start_page: Strona startowa - field_status: Status - field_subject: Temat - field_subproject: Podprojekt - field_summary: Podsumowanie - field_time_zone: Strefa czasowa - field_title: TytuÅ‚ - field_tracker: Typ zagadnienia - field_type: Typ - field_updated_on: Zmienione - field_url: URL - field_user: Użytkownik - field_value: Wartość - field_version: Wersja - field_vf_personnel: Personel - field_vf_watcher: Obserwator - general_csv_decimal_separator: ',' - general_csv_encoding: UTF-8 - general_csv_separator: ';' - general_first_day_of_week: '1' - general_lang_name: 'Polski' - general_pdf_encoding: UTF-8 - general_text_No: 'Nie' - general_text_Yes: 'Tak' - general_text_no: 'nie' - general_text_yes: 'tak' - gui_validation_error: 1 błąd - gui_validation_error_plural234: "%{count} błędy" - gui_validation_error_plural5: "%{count} błędów" - gui_validation_error_plural: "%{count} błędów" - label_activity: Aktywność - label_add_another_file: Dodaj kolejny plik - label_add_note: Dodaj notatkÄ™ - label_added: dodane - label_added_time_by: "Dodane przez %{author} %{age} temu" - label_administration: Administracja - label_age: Wiek - label_ago: dni temu - label_all: wszystko - label_all_time: caÅ‚y czas - label_all_words: Wszystkie sÅ‚owa - label_and_its_subprojects: "%{value} i podprojekty" - label_applied_status: Stosowany status - label_assigned_to_me_issues: Zagadnienia przypisane do mnie - label_associated_revisions: Skojarzone rewizje - label_attachment: Plik - label_attachment_delete: UsuÅ„ plik - label_attachment_new: Nowy plik - label_attachment_plural: Pliki - label_attribute: Atrybut - label_attribute_plural: Atrybuty - label_auth_source: Tryb identyfikacji - label_auth_source_new: Nowy tryb identyfikacji - label_auth_source_plural: Tryby identyfikacji - label_authentication: Identyfikacja - label_blocked_by: zablokowane przez - label_blocks: blokuje - label_board: Forum - label_board_new: Nowe forum - label_board_plural: Fora - label_boolean: Wartość logiczna - label_browse: PrzeglÄ…d - label_bulk_edit_selected_issues: Zbiorowa edycja zagadnieÅ„ - label_calendar: Kalendarz - label_change_plural: Zmiany - label_change_properties: ZmieÅ„ wÅ‚aÅ›ciwoÅ›ci - label_change_status: Status zmian - label_change_view_all: Pokaż wszystkie zmiany - label_changes_details: Szczegóły wszystkich zmian - label_changeset_plural: Zestawienia zmian - label_chronological_order: W kolejnoÅ›ci chronologicznej - label_closed_issues: zamkniÄ™te - label_closed_issues_plural234: zamkniÄ™te - label_closed_issues_plural5: zamkniÄ™te - label_closed_issues_plural: zamkniÄ™te - label_x_open_issues_abbr_on_total: - zero: 0 open / %{total} - one: 1 open / %{total} - other: "%{count} open / %{total}" - label_x_open_issues_abbr: - zero: 0 open - one: 1 open - other: "%{count} open" - label_x_closed_issues_abbr: - zero: 0 closed - one: 1 closed - other: "%{count} closed" - label_comment: Komentarz - label_comment_add: Dodaj komentarz - label_comment_added: Komentarz dodany - label_comment_delete: UsuÅ„ komentarze - label_comment_plural234: Komentarze - label_comment_plural5: Komentarze - label_comment_plural: Komentarze - label_x_comments: - zero: no comments - one: 1 comment - other: "%{count} comments" - label_commits_per_author: Zatwierdzenia wedÅ‚ug autorów - label_commits_per_month: Zatwierdzenia wedÅ‚ug miesiÄ™cy - label_confirmation: Potwierdzenie - label_contains: zawiera - label_copied: skopiowano - label_copy_workflow_from: Kopiuj przepÅ‚yw z - label_current_status: Obecny status - label_current_version: Obecna wersja - label_custom_field: Dowolne pole - label_custom_field_new: Nowe dowolne pole - label_custom_field_plural: Dowolne pola - label_date: Data - label_date_from: Z - label_date_range: Zakres datowy - label_date_to: Do - label_day_plural: dni - label_default: DomyÅ›lne - label_default_columns: DomyÅ›lne kolumny - label_deleted: usuniÄ™te - label_details: Szczegóły - label_diff_inline: w linii - label_diff_side_by_side: obok siebie - label_disabled: zablokowany - label_display_per_page: "Na stronÄ™: %{value}" - label_document: Dokument - label_document_added: Dodano dokument - label_document_new: Nowy dokument - label_document_plural: Dokumenty - label_download: "%{count} Pobranie" - label_download_plural234: "%{count} Pobrania" - label_download_plural5: "%{count} PobraÅ„" - label_download_plural: "%{count} Pobrania" - label_downloads_abbr: Pobieranie - label_duplicated_by: zduplikowane przez - label_duplicates: duplikuje - label_end_to_end: koniec do koÅ„ca - label_end_to_start: koniec do poczÄ…tku - label_enumeration_new: Nowa wartość - label_enumerations: Wyliczenia - label_environment: Åšrodowisko - label_equals: równa siÄ™ - label_example: PrzykÅ‚ad - label_export_to: Eksportuj do - label_f_hour: "%{value} godzina" - label_f_hour_plural: "%{value} godzin" - label_feed_plural: Ilość RSS - label_feeds_access_key_created_on: "Klucz dostÄ™pu RSS stworzony %{value} dni temu" - label_file_added: Dodano plik - label_file_plural: Pliki - label_filter_add: Dodaj filtr - label_filter_plural: Filtry - label_float: Liczba rzeczywista - label_follows: nastÄ™puje po - label_gantt: Gantt - label_general: Ogólne - label_generate_key: Wygeneruj klucz - label_help: Pomoc - label_history: Historia - label_home: Główna - label_in: w - label_in_less_than: mniejsze niż - label_in_more_than: wiÄ™ksze niż - label_incoming_emails: PrzychodzÄ…ca poczta elektroniczna - label_index_by_date: Indeks wg daty - label_index_by_title: Indeks - label_information: Informacja - label_information_plural: Informacje - label_integer: Liczba caÅ‚kowita - label_internal: WewnÄ™trzny - label_issue: Zagadnienie - label_issue_added: Dodano zagadnienie - label_issue_category: Kategoria zagadnienia - label_issue_category_new: Nowa kategoria - label_issue_category_plural: Kategorie zagadnieÅ„ - label_issue_new: Nowe zagadnienie - label_issue_plural: Zagadnienia - label_issue_status: Status zagadnienia - label_issue_status_new: Nowy status - label_issue_status_plural: Statusy zagadnieÅ„ - label_issue_tracking: Åšledzenie zagadnieÅ„ - label_issue_updated: Uaktualniono zagadnienie - label_issue_view_all: Zobacz wszystkie zagadnienia - label_issue_watchers: Obserwatorzy - label_issues_by: "Zagadnienia wprowadzone przez %{value}" - label_jump_to_a_project: Skocz do projektu... - label_language_based: Na podstawie jÄ™zyka - label_last_changes: "ostatnie %{count} zmian" - label_last_login: Ostatnie połączenie - label_last_month: ostatni miesiÄ…c - label_last_n_days: "ostatnie %{count} dni" - label_last_week: ostatni tydzieÅ„ - label_latest_revision: Najnowsza rewizja - label_latest_revision_plural: Najnowsze rewizje - label_ldap_authentication: Autoryzacja LDAP - label_less_than_ago: dni mniej - label_list: Lista - label_loading: Åadowanie... - label_logged_as: Zalogowany jako - label_login: Login - label_logout: Wylogowanie - label_max_size: Maksymalny rozmiar - label_me: ja - label_member: Uczestnik - label_member_new: Nowy uczestnik - label_member_plural: Uczestnicy - label_message_last: Ostatnia wiadomość - label_message_new: Nowa wiadomość - label_message_plural: WiadomoÅ›ci - label_message_posted: Dodano wiadomość - label_min_max_length: Min - Maks dÅ‚ugość - label_modification: "%{count} modyfikacja" - label_modification_plural234: "%{count} modyfikacje" - label_modification_plural5: "%{count} modyfikacji" - label_modification_plural: "%{count} modyfikacje" - label_modified: zmodyfikowane - label_module_plural: ModuÅ‚y - label_month: MiesiÄ…c - label_months_from: miesiÄ…ce od - label_more: WiÄ™cej - label_more_than_ago: dni wiÄ™cej - label_my_account: Moje konto - label_my_page: Moja strona - label_my_projects: Moje projekty - label_new: Nowy - label_new_statuses_allowed: Uprawnione nowe statusy - label_news: Komunikat - label_news_added: Dodano komunikat - label_news_latest: Ostatnie komunikaty - label_news_new: Dodaj komunikat - label_news_plural: Komunikaty - label_news_view_all: Pokaż wszystkie komunikaty - label_next: NastÄ™pne - label_no_change_option: (Bez zmian) - label_no_data: Brak danych do pokazania - label_nobody: nikt - label_none: brak - label_not_contains: nie zawiera - label_not_equals: różni siÄ™ - label_open_issues: otwarte - label_open_issues_plural234: otwarte - label_open_issues_plural5: otwarte - label_open_issues_plural: otwarte - label_optional_description: Opcjonalny opis - label_options: Opcje - label_overall_activity: Ogólna aktywność - label_overview: PrzeglÄ…d - label_password_lost: Zapomniane hasÅ‚o - label_per_page: Na stronÄ™ - label_permissions: Uprawnienia - label_permissions_report: Raport uprawnieÅ„ - label_personalize_page: Personalizuj tÄ… stronÄ™ - label_planning: Planowanie - label_please_login: Zaloguj siÄ™ - label_plugins: Wtyczki - label_precedes: poprzedza - label_preferences: Preferencje - label_preview: PodglÄ…d - label_previous: Poprzednie - label_project: Projekt - label_project_all: Wszystkie projekty - label_project_latest: Ostatnie projekty - label_project_new: Nowy projekt - label_project_plural234: Projekty - label_project_plural5: Projektów - label_project_plural: Projekty - label_x_projects: - zero: brak projektów - one: jeden projekt - other: "%{count} projektów" - label_public_projects: Projekty publiczne - label_query: Kwerenda - label_query_new: Nowa kwerenda - label_query_plural: Kwerendy - label_read: Czytanie... - label_register: Rejestracja - label_registered_on: Zarejestrowany - label_registration_activation_by_email: aktywacja konta przez e-mail - label_registration_automatic_activation: automatyczna aktywacja kont - label_registration_manual_activation: manualna aktywacja kont - label_related_issues: PowiÄ…zane zagadnienia - label_relates_to: powiÄ…zane z - label_relation_delete: UsuÅ„ powiÄ…zanie - label_relation_new: Nowe powiÄ…zanie - label_renamed: przemianowano - label_reply_plural: Odpowiedzi - label_report: Raport - label_report_plural: Raporty - label_reported_issues: Wprowadzone zagadnienia - label_repository: Repozytorium - label_repository_plural: Repozytoria - label_result_plural: Rezultatów - label_reverse_chronological_order: W kolejnoÅ›ci odwrotnej do chronologicznej - label_revision: Rewizja - label_revision_plural: Rewizje - label_roadmap: Mapa - label_roadmap_due_in: W czasie - label_roadmap_no_issues: Brak zagadnieÅ„ do tej wersji - label_roadmap_overdue: "%{value} spóźnienia" - label_role: Rola - label_role_and_permissions: Role i Uprawnienia - label_role_new: Nowa rola - label_role_plural: Role - label_scm: SCM - label_search: Szukaj - label_search_titles_only: Przeszukuj tylko tytuÅ‚y - label_send_information: WyÅ›lij informacjÄ™ użytkownikowi - label_send_test_email: WyÅ›lij próbny email - label_settings: Ustawienia - label_show_completed_versions: Pokaż kompletne wersje - label_sort_by: "Sortuj po %{value}" - label_sort_higher: Do góry - label_sort_highest: PrzesuÅ„ na górÄ™ - label_sort_lower: Do doÅ‚u - label_sort_lowest: PrzesuÅ„ na dół - label_spent_time: Przepracowany czas - label_start_to_end: poczÄ…tek do koÅ„ca - label_start_to_start: poczÄ…tek do poczÄ…tku - label_statistics: Statystyki - label_stay_logged_in: PozostaÅ„ zalogowany - label_string: Tekst - label_subproject_plural: Podprojekty - label_text: DÅ‚ugi tekst - label_theme: Temat - label_this_month: ten miesiÄ…c - label_this_week: ten tydzieÅ„ - label_this_year: ten rok - label_time_tracking: Åšledzenie czasu pracy - label_today: dzisiaj - label_topic_plural: Tematy - label_total: Ogółem - label_tracker: Typ zagadnienia - label_tracker_new: Nowy typ zagadnienia - label_tracker_plural: Typy zagadnieÅ„ - label_updated_time: "Zaktualizowane %{value} temu" - label_used_by: Używane przez - label_user: Użytkownik - label_user_mail_no_self_notified: "Nie chcÄ™ powiadomieÅ„ o zmianach, które sam wprowadzam." - label_user_mail_option_all: "Dla każdego zdarzenia w każdym moim projekcie" - label_user_mail_option_selected: "Tylko dla każdego zdarzenia w wybranych projektach..." - label_user_new: Nowy użytkownik - label_user_plural: Użytkownicy - label_version: Wersja - label_version_new: Nowa wersja - label_version_plural: Wersje - label_view_diff: Pokaż różnice - label_view_revisions: Pokaż rewizje - label_watched_issues: Obserwowane zagadnienia - label_week: TydzieÅ„ - label_wiki: Wiki - label_wiki_edit: Edycja wiki - label_wiki_edit_plural: Edycje wiki - label_wiki_page: Strona wiki - label_wiki_page_plural: Strony wiki - label_workflow: PrzepÅ‚yw - label_year: Rok - label_yesterday: wczoraj - mail_body_account_activation_request: "Zarejestrowano nowego użytkownika: (%{value}). Konto oczekuje na twoje zatwierdzenie:" - mail_body_account_information: Twoje konto - mail_body_account_information_external: "Możesz użyć twojego %{value} konta do zalogowania." - mail_body_lost_password: 'W celu zmiany swojego hasÅ‚a użyj poniższego odnoÅ›nika:' - mail_body_register: 'W celu aktywacji Twojego konta, użyj poniższego odnoÅ›nika:' - mail_body_reminder: "Wykaz przypisanych do Ciebie zagadnieÅ„, których termin wypada w ciÄ…gu nastÄ™pnych %{count} dni" - mail_subject_account_activation_request: "Zapytanie aktywacyjne konta %{value}" - mail_subject_lost_password: "Twoje hasÅ‚o do %{value}" - mail_subject_register: "Aktywacja konta w %{value}" - mail_subject_reminder: "Uwaga na terminy, masz zagadnienia do obsÅ‚użenia w ciÄ…gu nastÄ™pnych %{count} dni! (%{days})" - notice_account_activated: Twoje konto zostaÅ‚o aktywowane. Możesz siÄ™ zalogować. - notice_account_invalid_creditentials: ZÅ‚y użytkownik lub hasÅ‚o - notice_account_lost_email_sent: Email z instrukcjami zmiany hasÅ‚a zostaÅ‚ wysÅ‚any do Ciebie. - notice_account_password_updated: HasÅ‚o prawidÅ‚owo zmienione. - notice_account_pending: "Twoje konto zostaÅ‚o utworzone i oczekuje na zatwierdzenie administratora." - notice_account_register_done: Konto prawidÅ‚owo stworzone. - notice_account_unknown_email: Nieznany użytkownik. - notice_account_updated: Konto prawidÅ‚owo zaktualizowane. - notice_account_wrong_password: ZÅ‚e hasÅ‚o - notice_can_t_change_password: To konto ma zewnÄ™trzne źródÅ‚o identyfikacji. Nie możesz zmienić hasÅ‚a. - notice_default_data_loaded: DomyÅ›lna konfiguracja zostaÅ‚a pomyÅ›lnie zaÅ‚adowana. - notice_email_error: "WystÄ…piÅ‚ błąd w trakcie wysyÅ‚ania maila (%{value})" - notice_email_sent: "Email zostaÅ‚ wysÅ‚any do %{value}" - notice_failed_to_save_issues: "Błąd podczas zapisu zagadnieÅ„ %{count} z %{total} zaznaczonych: %{ids}." - notice_feeds_access_key_reseted: Twój klucz dostÄ™pu RSS zostaÅ‚ zrestetowany. - notice_file_not_found: Strona do której próbujesz siÄ™ dostać nie istnieje lub zostaÅ‚a usuniÄ™ta. - notice_locking_conflict: Dane poprawione przez innego użytkownika. - notice_no_issue_selected: "Nie wybrano zagadnienia! Zaznacz zagadnienie, które chcesz edytować." - notice_not_authorized: Nie jesteÅ› autoryzowany by zobaczyć stronÄ™. - notice_successful_connection: Udane nawiÄ…zanie połączenia. - notice_successful_create: Utworzenie zakoÅ„czone sukcesem. - notice_successful_delete: UsuniÄ™cie zakoÅ„czone sukcesem. - notice_successful_update: Uaktualnienie zakoÅ„czone sukcesem. - notice_unable_delete_version: Nie można usunąć wersji - permission_add_issue_notes: Dodawanie notatek - permission_add_issue_watchers: Dodawanie obserwatorów - permission_add_issues: Dodawanie zagadnieÅ„ - permission_add_messages: Dodawanie wiadomoÅ›ci - permission_browse_repository: PrzeglÄ…danie repozytorium - permission_comment_news: Komentowanie komunikatów - permission_commit_access: Wykonywanie zatwierdzeÅ„ - permission_delete_issues: Usuwanie zagadnieÅ„ - permission_delete_messages: Usuwanie wiadomoÅ›ci - permission_delete_wiki_pages: Usuwanie stron wiki - permission_delete_wiki_pages_attachments: Usuwanie załączników - permission_delete_own_messages: Usuwanie wÅ‚asnych wiadomoÅ›ci - permission_edit_issue_notes: Edycja notatek - permission_edit_issues: Edycja zagadnieÅ„ - permission_edit_messages: Edycja wiadomoÅ›ci - permission_edit_own_issue_notes: Edycja wÅ‚asnych notatek - permission_edit_own_messages: Edycja wÅ‚asnych wiadomoÅ›ci - permission_edit_own_time_entries: Edycja wÅ‚asnego dziennika - permission_edit_project: Edycja projektów - permission_edit_time_entries: Edycja wpisów dziennika - permission_edit_wiki_pages: Edycja stron wiki - permission_log_time: Zapisywanie przepracowanego czasu - permission_manage_boards: ZarzÄ…dzanie forami - permission_manage_categories: ZarzÄ…dzanie kategoriami zaganieÅ„ - permission_manage_documents: ZarzÄ…dzanie dokumentami - permission_manage_files: ZarzÄ…dzanie plikami - permission_manage_issue_relations: ZarzÄ…dzanie powiÄ…zaniami zagadnieÅ„ - permission_manage_members: ZarzÄ…dzanie uczestnikami - permission_manage_news: ZarzÄ…dzanie komunikatami - permission_manage_public_queries: ZarzÄ…dzanie publicznymi kwerendami - permission_manage_repository: ZarzÄ…dzanie repozytorium - permission_manage_versions: ZarzÄ…dzanie wersjami - permission_manage_wiki: ZarzÄ…dzanie wiki - permission_move_issues: Przenoszenie zagadnieÅ„ - permission_protect_wiki_pages: Blokowanie stron wiki - permission_rename_wiki_pages: Zmiana nazw stron wiki - permission_save_queries: Zapisywanie kwerend - permission_select_project_modules: Wybieranie modułów projektu - permission_view_calendar: PodglÄ…d kalendarza - permission_view_changesets: PodglÄ…d zmian - permission_view_documents: PodglÄ…d dokumentów - permission_view_files: PodglÄ…d plików - permission_view_gantt: PodglÄ…d diagramu Gantta - permission_view_issue_watchers: PodglÄ…d listy obserwatorów - permission_view_messages: PodglÄ…d wiadomoÅ›ci - permission_view_time_entries: PodglÄ…d przepracowanego czasu - permission_view_wiki_edits: PodglÄ…d historii wiki - permission_view_wiki_pages: PodglÄ…d wiki - project_module_boards: Fora - project_module_documents: Dokumenty - project_module_files: Pliki - project_module_issue_tracking: Åšledzenie zagadnieÅ„ - project_module_news: Komunikaty - project_module_repository: Repozytorium - project_module_time_tracking: Åšledzenie czasu pracy - project_module_wiki: Wiki - setting_activity_days_default: Dni wyÅ›wietlane w aktywnoÅ›ci projektu - setting_app_subtitle: PodtytuÅ‚ aplikacji - setting_app_title: TytuÅ‚ aplikacji - setting_attachment_max_size: Maks. rozm. załącznika - setting_autofetch_changesets: Automatyczne pobieranie zmian - setting_autologin: Auto logowanie - setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) - setting_commit_fix_keywords: SÅ‚owa zmieniajÄ…ce status - setting_commit_ref_keywords: SÅ‚owa tworzÄ…ce powiÄ…zania - setting_cross_project_issue_relations: Zezwól na powiÄ…zania zagadnieÅ„ miÄ™dzy projektami - setting_date_format: Format daty - setting_default_language: DomyÅ›lny jÄ™zyk - setting_default_projects_public: Nowe projekty sÄ… domyÅ›lnie publiczne - setting_display_subprojects_issues: DomyÅ›lnie pokazuj zagadnienia podprojektów w głównym projekcie - setting_emails_footer: Stopka e-mail - setting_enabled_scm: DostÄ™pny SCM - setting_feeds_limit: Limit danych RSS - setting_gravatar_enabled: Używaj ikon użytkowników Gravatar - setting_host_name: Nazwa hosta i Å›cieżka - setting_issue_list_default_columns: DomyÅ›lne kolumny wyÅ›wietlane na liÅ›cie zagadnieÅ„ - setting_issues_export_limit: Limit eksportu zagadnieÅ„ - setting_login_required: Identyfikacja wymagana - setting_mail_from: Adres email wysyÅ‚ki - setting_mail_handler_api_enabled: Uaktywnij usÅ‚ugi sieciowe (WebServices) dla poczty przychodzÄ…cej - setting_mail_handler_api_key: Klucz API - setting_per_page_options: Opcje iloÅ›ci obiektów na stronie - setting_plain_text_mail: tylko tekst (bez HTML) - setting_protocol: ProtokoÅ‚ - setting_self_registration: Samodzielna rejestracja użytkowników - setting_sequential_project_identifiers: Generuj sekwencyjne identyfikatory projektów - setting_sys_api_enabled: Włączenie WS do zarzÄ…dzania repozytorium - setting_text_formatting: Formatowanie tekstu - setting_time_format: Format czasu - setting_user_format: Personalny format wyÅ›wietlania - setting_welcome_text: Tekst powitalny - setting_wiki_compression: Kompresja historii Wiki - status_active: aktywny - status_locked: zablokowany - status_registered: zarejestrowany - text_are_you_sure: JesteÅ› pewien ? - text_assign_time_entries_to_project: Przypisz wpisy dziennika do projektu - text_caracters_maximum: "%{count} znaków maksymalnie." - text_caracters_minimum: "Musi być nie krótsze niż %{count} znaków." - text_comma_separated: Wielokrotne wartoÅ›ci dozwolone (rozdzielone przecinkami). - text_default_administrator_account_changed: Zmieniono domyÅ›lne hasÅ‚o administratora - text_destroy_time_entries: UsuÅ„ wpisy dziennika - text_destroy_time_entries_question: Przepracowano %{hours} godzin przy zagadnieniu, które chcesz usunąć. Co chcesz zrobić? - text_email_delivery_not_configured: "Dostarczanie poczty elektronicznej nie zostaÅ‚o skonfigurowane, wiÄ™c powiadamianie jest nieaktywne.\nSkonfiguruj serwer SMTP w config/configuration.yml a nastÄ™pnie zrestartuj aplikacjÄ™ i uaktywnij to." - text_enumeration_category_reassign_to: 'ZmieÅ„ przypisanie na tÄ… wartość:' - text_enumeration_destroy_question: "%{count} obiektów jest przypisana do tej wartoÅ›ci." - text_file_repository_writable: Zapisywalne repozytorium plików - text_issue_added: "Zagadnienie %{id} zostaÅ‚o wprowadzone (by %{author})." - text_issue_category_destroy_assignments: UsuÅ„ przydziaÅ‚y kategorii - text_issue_category_destroy_question: "Zagadnienia (%{count}) sÄ… przypisane do tej kategorii. Co chcesz zrobić?" - text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii - text_issue_updated: "Zagadnienie %{id} zostaÅ‚o zaktualizowane (by %{author})." - text_issues_destroy_confirmation: 'Czy jestes pewien, że chcesz usunąć wskazane zagadnienia?' - text_issues_ref_in_commit_messages: OdwoÅ‚ania do zagadnieÅ„ w komentarzach zatwierdzeÅ„ - text_length_between: "DÅ‚ugość pomiÄ™dzy %{min} i %{max} znaków." - text_load_default_configuration: ZaÅ‚aduj domyÅ›lnÄ… konfiguracjÄ™ - text_min_max_length_info: 0 oznacza brak restrykcji - text_no_configuration_data: "Role użytkowników, typy zagadnieÅ„, statusy zagadnieÅ„ oraz przepÅ‚yw pracy nie zostaÅ‚y jeszcze skonfigurowane.\nJest wysoce rekomendowane by zaÅ‚adować domyÅ›lnÄ… konfiguracjÄ™. Po zaÅ‚adowaniu bÄ™dzie możliwość edycji tych danych." - text_project_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć ten projekt i wszystkie powiÄ…zane dane? - text_reassign_time_entries: 'Przepnij przepracowany czas do tego zagadnienia:' - text_regexp_info: np. ^[A-Z0-9]+$ - text_repository_usernames_mapping: "Wybierz lub uaktualnij przyporzÄ…dkowanie użytkowników Redmine do użytkowników repozytorium.\nUżytkownicy z takÄ… samÄ… nazwÄ… lub adresem email sÄ… przyporzÄ…dkowani automatycznie." - text_rmagick_available: RMagick dostÄ™pne (opcjonalnie) - text_select_mail_notifications: Zaznacz czynnoÅ›ci przy których użytkownik powinien być powiadomiony mailem. - text_select_project_modules: 'Wybierz moduÅ‚y do aktywacji w tym projekcie:' - text_status_changed_by_changeset: "Zastosowane w zmianach %{value}." - text_subprojects_destroy_warning: "Podprojekt(y): %{value} zostanÄ… także usuniÄ™te." - text_tip_issue_begin_day: zadanie zaczynajÄ…ce siÄ™ dzisiaj - text_tip_issue_begin_end_day: zadanie zaczynajÄ…ce i koÅ„czÄ…ce siÄ™ dzisiaj - text_tip_issue_end_day: zadanie koÅ„czÄ…ce siÄ™ dzisiaj - text_tracker_no_workflow: Brak przepÅ‚ywu zdefiniowanego dla tego typu zagadnienia - text_unallowed_characters: Niedozwolone znaki - text_user_mail_option: "W przypadku niezaznaczonych projektów, bÄ™dziesz otrzymywaÅ‚ powiadomienia tylko na temat zagadnieÅ„, które obserwujesz, lub w których bierzesz udziaÅ‚ (np. jesteÅ› autorem lub adresatem)." - text_user_wrote: "%{value} napisaÅ‚:" - text_wiki_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć to wiki i całą jego zawartość ? - text_workflow_edit: Zaznacz rolÄ™ i typ zagadnienia do edycji przepÅ‚ywu - - label_user_activity: "Aktywność: %{value}" - label_updated_time_by: "Uaktualnione przez %{author} %{age} temu" - text_diff_truncated: '... Ten plik różnic zostaÅ‚ przyciÄ™ty ponieważ jest zbyt dÅ‚ugi.' - setting_diff_max_lines_displayed: Maksymalna liczba linii różnicy do pokazania - text_plugin_assets_writable: Zapisywalny katalog zasobów wtyczek - warning_attachments_not_saved: "%{count} załącznik(ów) nie zostaÅ‚o zapisanych." - field_editable: Edytowalne - label_display: WyglÄ…d - button_create_and_continue: Stwórz i dodaj kolejne - text_custom_field_possible_values_info: 'Każda wartość w osobnej linii' - setting_repository_log_display_limit: Maksymalna liczba rewizji pokazywanych w logu pliku - setting_file_max_size_displayed: Maksymalny rozmiar plików tekstowych osadzanych w stronie - field_watcher: Obserwator - setting_openid: Logowanie i rejestracja przy użyciu OpenID - field_identity_url: Identyfikator OpenID (URL) - label_login_with_open_id_option: albo użyj OpenID - field_content: Treść - label_descending: MalejÄ…co - label_sort: Sortuj - label_ascending: RosnÄ…co - label_date_from_to: Od %{start} do %{end} - label_greater_or_equal: ">=" - label_less_or_equal: <= - text_wiki_page_destroy_question: Ta strona posiada podstrony (%{descendants}). Co chcesz zrobić? - text_wiki_page_reassign_children: Podepnij je do strony nadrzÄ™dnej wzglÄ™dem usuwanej - text_wiki_page_nullify_children: PrzesuÅ„ je na szczyt hierarchii - text_wiki_page_destroy_children: UsuÅ„ wszystkie podstrony - setting_password_min_length: Minimalna dÅ‚ugość hasÅ‚a - field_group_by: Grupuj wyniki wg - mail_subject_wiki_content_updated: "Strona wiki '%{id}' zostaÅ‚a uaktualniona" - label_wiki_content_added: Dodano stronÄ™ wiki - mail_subject_wiki_content_added: "Strona wiki '%{id}' zostaÅ‚a dodana" - mail_body_wiki_content_added: Strona wiki '%{id}' zostaÅ‚a dodana przez %{author}. - label_wiki_content_updated: Uaktualniono stronÄ™ wiki - mail_body_wiki_content_updated: Strona wiki '%{id}' zostaÅ‚a uaktualniona przez %{author}. - permission_add_project: Tworzenie projektu - setting_new_project_user_role_id: Rola nadawana twórcom projektów, którzy nie posiadajÄ… uprawnieÅ„ administatora - label_view_all_revisions: Pokaż wszystkie rewizje - label_tag: SÅ‚owo kluczowe - label_branch: Gałąź - error_no_tracker_in_project: Projekt nie posiada powiÄ…zanych typów zagadnieÅ„. Sprawdź ustawienia projektu. - error_no_default_issue_status: Nie zdefiniowano domyÅ›lnego statusu zagadnieÅ„. Sprawdź konfiguracjÄ™ (Przejdź do "Administracja -> Statusy zagadnieÅ„). - text_journal_changed: "Zmieniono %{label} z %{old} na %{new}" - text_journal_set_to: "Ustawiono %{label} na %{value}" - text_journal_deleted: "UsuniÄ™to %{label} (%{old})" - label_group_plural: Grupy - label_group: Grupa - label_group_new: Nowa grupa - label_time_entry_plural: Przepracowany czas - text_journal_added: "Dodano %{label} %{value}" - field_active: Aktywne - enumeration_system_activity: Aktywność Systemowa - button_copy_and_follow: Kopiuj i przejdź do kopii zagadnienia - button_duplicate: Duplikuj - button_move_and_follow: PrzenieÅ› i przejdź do zagadnienia - button_show: Pokaż - error_can_not_archive_project: Ten projekt nie może zostać zarchiwizowany - error_can_not_reopen_issue_on_closed_version: Zagadnienie przydzielone do zakoÅ„czonej wersji nie może zostać ponownie otwarte - error_issue_done_ratios_not_updated: "% wykonania zagadnienia nie zostaÅ‚ uaktualniony." - error_workflow_copy_source: ProszÄ™ wybrać źródÅ‚owy typ zagadnienia lub rolÄ™ - error_workflow_copy_target: ProszÄ™ wybrać docelowe typ(y) zagadnieÅ„ i rolÄ™(e) - field_sharing: Współdzielenie - label_api_access_key: Klucz dostÄ™pu do API - label_api_access_key_created_on: Klucz dostÄ™pu do API zostaÅ‚ utworzony %{value} temu - label_close_versions: Zamknij ukoÅ„czone wersje - label_copy_same_as_target: Jak cel - label_copy_source: ŹródÅ‚o - label_copy_target: Cel - label_display_used_statuses_only: WyÅ›wietlaj tylko statusy używane przez ten typ zagadnienia - label_feeds_access_key: Klucz dostÄ™pu do RSS - label_missing_api_access_key: Brakuje klucza dostÄ™pu do API - label_missing_feeds_access_key: Brakuje klucza dostÄ™pu do RSS - label_revision_id: Rewizja %{value} - label_subproject_new: Nowy podprojekt - label_update_issue_done_ratios: Uaktualnij % wykonania - label_user_anonymous: Anonimowy - label_version_sharing_descendants: Z podprojektami - label_version_sharing_hierarchy: Z hierarchiÄ… projektów - label_version_sharing_none: Brak współdzielenia - label_version_sharing_system: Ze wszystkimi projektami - label_version_sharing_tree: Z drzewem projektów - notice_api_access_key_reseted: Twój klucz dostÄ™pu do API zostaÅ‚ zresetowany. - notice_issue_done_ratios_updated: Uaktualnienie % wykonania zakoÅ„czone sukcesem. - permission_add_subprojects: Tworzenie podprojektów - permission_delete_issue_watchers: UsuÅ„ obserwatorów - permission_view_issues: PrzeglÄ…danie zagadnieÅ„ - setting_default_projects_modules: DomyÅ›lnie włączone moduÅ‚y dla nowo tworzonych projektów - setting_gravatar_default: DomyÅ›lny obraz Gravatar - setting_issue_done_ratio: Obliczaj postÄ™p realizacji zagadnieÅ„ za pomocÄ… - setting_issue_done_ratio_issue_field: "% Wykonania zagadnienia" - setting_issue_done_ratio_issue_status: Statusu zagadnienia - setting_mail_handler_body_delimiters: Przycinaj e-maile po jednej z tych linii - setting_rest_api_enabled: Uaktywnij usÅ‚ugÄ™ sieciowÄ… REST - setting_start_of_week: Pierwszy dzieÅ„ tygodnia - text_line_separated: Dozwolone jest wiele wartoÅ›ci (każda wartość w osobnej linii). - text_own_membership_delete_confirmation: |- - Masz zamiar usunąć niektóre lub wszystkie swoje uprawnienia. Po wykonaniu tej czynnoÅ›ci możesz utracić możliwoÅ›ci edycji tego projektu. - Czy na pewno chcesz kontynuować? - version_status_closed: zamkniÄ™ta - version_status_locked: zablokowana - version_status_open: otwarta - - label_board_sticky: Przyklejona - label_board_locked: ZamkniÄ™ta - permission_export_wiki_pages: Eksport stron wiki - permission_manage_project_activities: ZarzÄ…dzanie aktywnoÅ›ciami projektu - setting_cache_formatted_text: Buforuj sformatowany tekst - error_unable_delete_issue_status: Nie można usunąć statusu zagadnienia - label_profile: Profil - permission_manage_subtasks: ZarzÄ…dzanie podzagadnieniami - field_parent_issue: Zagadnienie nadrzÄ™dne - label_subtask_plural: Podzagadnienia - label_project_copy_notifications: WyÅ›lij powiadomienia mailowe przy kopiowaniu projektu - error_can_not_delete_custom_field: Nie można usunąć tego pola - error_unable_to_connect: Nie można połączyć (%{value}) - error_can_not_remove_role: Ta rola przypisana jest niektórym użytkownikom i nie może zostać usuniÄ™ta. - error_can_not_delete_tracker: Ten typ przypisany jest do części zagadnieÅ„ i nie może zostać usuniÄ™ty. - field_principal: PrzeÅ‚ożony - label_my_page_block: Elementy - notice_failed_to_save_members: "Nie można zapisać uczestników: %{errors}." - text_zoom_out: Zmniejsz czcionkÄ™ - text_zoom_in: PowiÄ™ksz czcionkÄ™ - notice_unable_delete_time_entry: Nie można usunąć wpisu z dziennika. - label_overall_spent_time: Przepracowany czas - field_time_entries: Dziennik - project_module_gantt: Diagram Gantta - project_module_calendar: Kalendarz - button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" - field_text: Text field - label_user_mail_option_only_owner: Only for things I am the owner of - setting_default_notification_option: Default notification option - label_user_mail_option_only_my_events: Only for things I watch or I'm involved in - label_user_mail_option_only_assigned: Only for things I am assigned to - label_user_mail_option_none: No events - field_member_of_group: Assignee's group - field_assigned_to_role: Assignee's role - notice_not_authorized_archived_project: The project you're trying to access has been archived. - label_principal_search: "Search for user or group:" - label_user_search: "Search for user:" - field_visible: Visible - setting_emails_header: Emails header - setting_commit_logtime_activity_id: Activity for logged time - text_time_logged_by_changeset: Applied in changeset %{value}. - setting_commit_logtime_enabled: Enable time logging - notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) - setting_gantt_items_limit: Maximum number of items displayed on the gantt chart - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: 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: Kodowanie komentarzy zatwierdzeÅ„ - 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 - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - 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 zagadnienie - one: 1 zagadnienie - other: "%{count} zagadnienia" - 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.
Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account - text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: wszystko - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: Z podprojektami - label_cross_project_tree: Z drzewem projektów - label_cross_project_hierarchy: Z hierarchiÄ… projektów - label_cross_project_system: Ze wszystkimi projektami - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/042891243ed00da7c541ac0d9a5f19f90a344ef6.svn-base --- a/.svn/pristine/04/042891243ed00da7c541ac0d9a5f19f90a344ef6.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1133 +0,0 @@ -# Korean translations for Ruby on Rails -ko: - direction: ltr - 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: "%m/%d" - long: "%Yë…„ %mì›” %dì¼ (%a)" - - day_names: [ì¼ìš”ì¼, 월요ì¼, 화요ì¼, 수요ì¼, 목요ì¼, 금요ì¼, 토요ì¼] - abbr_day_names: [ì¼, ì›”, í™”, 수, 목, 금, 토] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, 1ì›”, 2ì›”, 3ì›”, 4ì›”, 5ì›”, 6ì›”, 7ì›”, 8ì›”, 9ì›”, 10ì›”, 11ì›”, 12ì›”] - 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/%m/%d %H:%M:%S" - time: "%H:%M" - short: "%y/%m/%d %H:%M" - long: "%Yë…„ %Bì›” %dì¼, %H시 %Më¶„ %Sì´ˆ %Z" - am: "오전" - pm: "오후" - - datetime: - distance_in_words: - half_a_minute: "30ì´ˆ" - 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 hour" - other: "%{count} hours" - 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}ë…„" - prompts: - year: "ë…„" - month: "ì›”" - day: "ì¼" - hour: "시" - minute: "ë¶„" - second: "ì´ˆ" - - number: - # Used in number_with_delimiter() - # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' - format: - # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) - separator: "." - # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three) - delimiter: "," - # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) - precision: 3 - - # Used in number_to_currency() - currency: - format: - # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) - format: "%u%n" - unit: "â‚©" - # These three are to override number.format and are optional - separator: "." - delimiter: "," - precision: 0 - - # Used in number_to_percentage() - percentage: - format: - # These three are to override number.format and are optional - # separator: - delimiter: "" - # precision: - - # Used in number_to_precision() - precision: - format: - # These three are to override number.format and are optional - # separator: - delimiter: "" - # precision: - - # Used in number_to_human_size() - human: - format: - # These three are to override number.format and are optional - # separator: - 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: - words_connector: ", " - two_words_connector: "ê³¼ " - last_word_connector: ", " - sentence_connector: "그리고" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "í•œê°œì˜ ì˜¤ë¥˜ê°€ ë°œìƒí•´ %{model}ì„(를) 저장하지 않았습니다." - other: "%{count}ê°œì˜ ì˜¤ë¥˜ê°€ ë°œìƒí•´ %{model}ì„(를) 저장하지 않았습니다." - # The variable :count is also available - body: "ë‹¤ìŒ í•­ëª©ì— ë¬¸ì œê°€ 발견했습니다:" - - messages: - inclusion: "ì€ ëª©ë¡ì— í¬í•¨ë˜ì–´ 있지 않습니다" - exclusion: "ì€ ì˜ˆì•½ë˜ì–´ 있습니다" - invalid: "ì€ ìœ íš¨í•˜ì§€ 않습니다." - confirmation: "ì€ í™•ì¸ì´ ë˜ì§€ 않았습니다" - accepted: "ì€ ì¸ì •ë˜ì–´ì•¼ 합니다" - empty: "ì€ ê¸¸ì´ê°€ 0ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤." - blank: "ì€ ë¹ˆ ê°’ì´ì–´ì„œëŠ” 안 ë©ë‹ˆë‹¤" - too_long: "ì€ ë„ˆë¬´ ê¹ë‹ˆë‹¤ (최대 %{count}ìž ê¹Œì§€)" - too_short: "ì€ ë„ˆë¬´ 짧습니다 (최소 %{count}ìž ê¹Œì§€)" - wrong_length: "ì€ ê¸¸ì´ê°€ 틀렸습니다 (%{count}ìžì´ì–´ì•¼ 합니다.)" - taken: "ì€ ì´ë¯¸ ì„ íƒëœ ê²ë‹ˆë‹¤" - not_a_number: "ì€ ìˆ«ìžê°€ 아닙니다" - 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: 'Korean (한국어)' - general_csv_separator: ',' - general_csv_decimal_separator: '.' - general_csv_encoding: CP949 - general_pdf_encoding: CP949 - 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_email_sent: "%{value}님ì—게 ë©”ì¼ì´ 발송ë˜ì—ˆìŠµë‹ˆë‹¤." - notice_email_error: "ë©”ì¼ì„ 전송하는 ê³¼ì •ì— ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. (%{value})" - notice_feeds_access_key_reseted: RSSì— ì ‘ê·¼ê°€ëŠ¥í•œ 열쇠(key)ê°€ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. - notice_failed_to_save_issues: "ì €ìž¥ì— ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤: 실패 %{count}(ì„ íƒ %{total}): %{ids}." - notice_no_issue_selected: "ì¼ê°ì´ ì„ íƒë˜ì§€ 않았습니다. 수정하기 ì›í•˜ëŠ” ì¼ê°ì„ ì„ íƒí•˜ì„¸ìš”" - notice_account_pending: "ê³„ì •ì´ ë§Œë“¤ì–´ì¡Œìœ¼ë©° ê´€ë¦¬ìž ìŠ¹ì¸ ëŒ€ê¸°ì¤‘ìž…ë‹ˆë‹¤." - notice_default_data_loaded: ê¸°ë³¸ê°’ì„ ì„±ê³µì ìœ¼ë¡œ ì½ì–´ë“¤ì˜€ìŠµë‹ˆë‹¤. - notice_unable_delete_version: 삭제할 수 없는 버전입니다. - - 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: 'ì¼ê°ì´ 없거나 ì´ í”„ë¡œì íŠ¸ì˜ ê²ƒì´ ì•„ë‹™ë‹ˆë‹¤.' - - 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_body_reminder: "ë‹¹ì‹ ì´ ë§¡ê³  있는 ì¼ê° %{count}ê°œì˜ ì™„ë£Œ ê¸°í•œì´ %{days}ì¼ í›„ 입니다." - mail_subject_reminder: "ë‚´ì¼ì´ ë§Œê¸°ì¸ ì¼ê° %{count}ê°œ (%{days})" - mail_subject_wiki_content_added: "위키페ì´ì§€ '%{id}'ì´(ê°€) 추가ë˜ì—ˆìŠµë‹ˆë‹¤." - mail_subject_wiki_content_updated: "'위키페ì´ì§€ %{id}'ì´(ê°€) 수정ë˜ì—ˆìŠµë‹ˆë‹¤." - mail_body_wiki_content_added: "%{author}ì´(ê°€) 위키페ì´ì§€ '%{id}'ì„(를) 추가하였습니다." - mail_body_wiki_content_updated: "%{author}ì´(ê°€) 위키페ì´ì§€ '%{id}'ì„(를) 수정하였습니다." - - gui_validation_error: ì—러 - gui_validation_error_plural: "%{count}ê°œ ì—러" - - 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_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: URL - field_start_page: 첫 페ì´ì§€ - field_subproject: 하위 프로ì íЏ - field_hours: 시간 - field_activity: 작업종류 - field_spent_on: 작업시간 - field_identifier: ì‹ë³„ìž - field_is_filter: 검색조건으로 ì‚¬ìš©ë¨ - field_issue_to_id: ì—°ê´€ëœ ì¼ê° - field_delay: 지연 - field_assignable: ì´ ì—­í• ì—게 ì¼ê°ì„ 맡길 수 ìžˆìŒ - field_redirect_existing_links: ê¸°ì¡´ì˜ ë§í¬ë¡œ ëŒë ¤ë³´ëƒ„(redirect) - field_estimated_hours: 추정시간 - field_column_names: 컬럼 - field_default_value: 기본값 - field_time_zone: 시간대 - field_searchable: 검색가능 - field_comments_sorting: 댓글 ì •ë ¬ - field_parent_title: ìƒìœ„ 제목 - field_editable: 편집가능 - field_watcher: ì¼ê°ì§€í‚´ì´ - field_identity_url: OpenID URL - field_content: ë‚´ìš© - field_group_by: 결과를 묶어 보여줄 기준 - - 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: 커밋(commit)ëœ ë³€ê²½ë¬¶ìŒì„ ìžë™ìœ¼ë¡œ 가져오기 - setting_sys_api_enabled: 저장소 ê´€ë¦¬ì— WS를 사용 - 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_footer: ë©”ì¼ ê¼¬ë¦¬ - setting_protocol: 프로토콜 - setting_per_page_options: 목ë¡ì—서, 한 페ì´ì§€ì— 표시할 í–‰ - setting_user_format: ì‚¬ìš©ìž í‘œì‹œ í˜•ì‹ - setting_activity_days_default: 프로ì íЏ ìž‘ì—…ë‚´ì—­ì— í‘œì‹œí•  기간 - setting_display_subprojects_issues: 하위 프로ì íŠ¸ì˜ ì¼ê°ì„ 함께 표시 - setting_enabled_scm: "ì§€ì›í•  SCM(Source Control Management)" - setting_mail_handler_api_enabled: 수신 ë©”ì¼ì— WS를 허용 - setting_mail_handler_api_key: API 키 - setting_sequential_project_identifiers: 프로ì íЏ ì‹ë³„ìžë¥¼ 순차ì ìœ¼ë¡œ ìƒì„± - setting_gravatar_enabled: ê·¸ë¼ë°”타 ì‚¬ìš©ìž ì•„ì´ì½˜ 사용 - setting_diff_max_lines_displayed: ì°¨ì´ì (diff) ë³´ê¸°ì— í‘œì‹œí•  최대 줄수 - setting_repository_log_display_limit: 저장소 ë³´ê¸°ì— í‘œì‹œí•  ê°œì •íŒ ì´ë ¥ì˜ 최대 갯수 - setting_file_max_size_displayed: 바로 보여줄 í…스트파ì¼ì˜ 최대 í¬ê¸° - setting_openid: OpenID 로그ì¸ê³¼ ë“±ë¡ í—ˆìš© - setting_password_min_length: 최소 암호 ê¸¸ì´ - setting_new_project_user_role_id: 프로ì íŠ¸ë¥¼ 만든 사용ìžì—게 주어질 ì—­í•  - - permission_add_project: 프로ì íЏ ìƒì„± - permission_edit_project: 프로ì íЏ 편집 - permission_select_project_modules: 프로ì íЏ 모듈 ì„ íƒ - permission_manage_members: êµ¬ì„±ì› ê´€ë¦¬ - permission_manage_versions: 버전 관리 - permission_manage_categories: ì¼ê° 범주 관리 - 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: Gantt차트 보기 - permission_view_calendar: 달력 보기 - permission_view_issue_watchers: ì¼ê°ì§€í‚´ì´ 보기 - permission_add_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_manage_documents: 문서 관리 - 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: ìžê¸° 메시지 ì‚­ì œ - - 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: ê²Œì‹œíŒ - - label_user: ì‚¬ìš©ìž - label_user_plural: ì‚¬ìš©ìž - label_user_new: 새 ì‚¬ìš©ìž - label_project: 프로ì íЏ - label_project_new: 새 프로ì íЏ - label_project_plural: 프로ì íЏ - label_x_projects: - zero: ì—†ìŒ - one: "한 프로ì íЏ" - 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_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_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_download: "%{count}회 다운로드" - label_download_plural: "%{count}회 다운로드" - 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_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: "ì´ %{total} ê±´ ëª¨ë‘ ì™„ë£Œ" - one: "한 ê±´ ì§„í–‰ 중 / ì´ %{total} ê±´ 중 " - other: "%{count} ê±´ ì§„í–‰ 중 / ì´ %{total} ê±´" - label_x_open_issues_abbr: - zero: ëª¨ë‘ ì™„ë£Œ - one: 한 ê±´ ì§„í–‰ 중 - other: "%{count} ê±´ ì§„í–‰ 중" - label_x_closed_issues_abbr: - zero: ëª¨ë‘ ë¯¸ì™„ë£Œ - one: 한 ê±´ 완료 - 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: Gantt 챠트 - label_internal: ë‚´ë¶€ - label_last_changes: "최근 %{count}ê°œì˜ ë³€ê²½ì‚¬í•­" - label_change_view_all: 모든 변경 ë‚´ì—­ 보기 - label_personalize_page: 입맛대로 구성하기 - label_comment: 댓글 - label_comment_plural: 댓글 - label_x_comments: - zero: 댓글 ì—†ìŒ - one: 한 ê°œì˜ ëŒ“ê¸€ - 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_modification: "%{count} 변경" - label_modification_plural: "%{count} 변경" - label_revision: ê°œì •íŒ - label_revision_plural: ê°œì •íŒ - label_associated_revisions: ê´€ë ¨ëœ ê°œì •íŒë“¤ - label_added: ì¶”ê°€ë¨ - label_modified: ë³€ê²½ë¨ - label_copied: ë³µì‚¬ë¨ - label_renamed: ì´ë¦„바뀜 - label_deleted: ì‚­ì œë¨ - label_latest_revision: 최근 ê°œì •íŒ - label_latest_revision_plural: 최근 ê°œì •íŒ - label_view_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: 피드(Feeds) - label_changes_details: 모든 ìƒì„¸ 변경 ë‚´ì—­ - label_issue_tracking: ì¼ê° ì¶”ì  - label_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_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_created_on: "피드 ì ‘ê·¼ 키가 %{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_no_self_notified: "ë‚´ê°€ 만든 ë³€ê²½ì‚¬í•­ë“¤ì— ëŒ€í•´ì„œëŠ” 알림메ì¼ì„ 받지 않습니다." - label_registration_activation_by_email: ë©”ì¼ë¡œ ê³„ì •ì„ í™œì„±í™”í•˜ê¸° - label_registration_automatic_activation: ìžë™ 계정 활성화 - label_registration_manual_activation: ìˆ˜ë™ ê³„ì • 활성화 - label_display_per_page: "페ì´ì§€ë‹¹ 줄수: %{value}" - label_age: 마지막 ìˆ˜ì •ì¼ - label_change_properties: ì†ì„± 변경 - label_general: ì¼ë°˜ - label_more: 제목 ë° ì„¤ëª… 수정 - label_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: 키 ìƒì„± - 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: 위키페ì´ì§€ 수정 - - 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_add: 추가 - button_change: 변경 - button_apply: ì ìš© - button_clear: 지우기 - button_lock: 잠금 - button_unlock: 잠금해제 - button_download: 다운로드 - button_list: ëª©ë¡ - button_view: 보기 - button_move: ì´ë™ - 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_annotate: ì´ë ¥í•´ì„¤ - button_update: 수정 - button_configure: 설정 - button_quote: 댓글달기 - - status_active: 사용중 - status_registered: 등ë¡ëŒ€ê¸° - status_locked: ìž ê¹€ - - 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_tip_issue_begin_day: 오늘 시작하는 업무(task) - text_tip_issue_end_day: 오늘 종료하는 업무(task) - text_tip_issue_begin_end_day: 오늘 시작하고 종료하는 업무(task) - text_caracters_maximum: "최대 %{count} ê¸€ìž ê°€ëŠ¥" - text_caracters_minimum: "최소한 %{count} ê¸€ìž ì´ìƒì´ì–´ì•¼ 합니다." - text_length_between: "%{min} ì—서 %{max} 글ìž" - text_tracker_no_workflow: ì´ ì¼ê° 유형ì—는 업무íë¦„ì´ ì •ì˜ë˜ì§€ 않았습니다. - text_unallowed_characters: 허용ë˜ì§€ 않는 문ìžì—´ - text_comma_separated: "구분ìž','를 ì´ìš©í•´ì„œ 여러 ê°œì˜ ê°’ì„ ìž…ë ¥í•  수 있습니다." - text_issues_ref_in_commit_messages: 커밋 메시지ì—서 ì¼ê°ì„ 참조하거나 해결하기 - text_issue_added: "%{author}ì´(ê°€) ì¼ê° %{id}ì„(를) 보고하였습니다." - text_issue_updated: "%{author}ì´(ê°€) ì¼ê° %{id}ì„(를) 수정하였습니다." - 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_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_category_reassign_to: '새로운 ê°’ì„ ì„¤ì •:' - text_enumeration_destroy_question: "%{count} ê°œì˜ ì¼ê°ì´ ì´ ê°’ì„ ì‚¬ìš©í•˜ê³  있습니다." - text_email_delivery_not_configured: "ì´ë©”ì¼ ì „ë‹¬ì´ ì„¤ì •ë˜ì§€ 않았습니다. 그래서 ì•Œë¦¼ì´ ë¹„í™œì„±í™”ë˜ì—ˆìŠµë‹ˆë‹¤.\n SMTP서버를 config/configuration.ymlì—서 설정하고 어플리케ì´ì…˜ì„ 다시 시작하십시오. 그러면 ë™ìž‘합니다." - text_repository_usernames_mapping: "저장소 로그ì—서 ë°œê²¬ëœ ê° ì‚¬ìš©ìžì— ë ˆë“œë§ˆì¸ ì‚¬ìš©ìžë¥¼ ì—…ë°ì´íŠ¸í• ë•Œ ì„ íƒí•©ë‹ˆë‹¤.\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: 하위 페ì´ì§€ë¥¼ ì´ íŽ˜ì´ì§€ 아래로 지정 - - 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: 작업분류(시간추ì ) - - field_issue_to: 관련 ì¼ê° - label_view_all_revisions: 모든 ê°œì •íŒ í‘œì‹œ - label_tag: 태그(Tag) - label_branch: 브랜치(Branch) - error_no_tracker_in_project: 사용할 수 있ë„ë¡ ì„¤ì •ëœ ì¼ê° ìœ í˜•ì´ ì—†ìŠµë‹ˆë‹¤. 프로ì íЏ ì„¤ì •ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤. - error_no_default_issue_status: '기본 ìƒíƒœê°€ ì •í•´ì ¸ 있지 않습니다. ì„¤ì •ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤. (주 ë©”ë‰´ì˜ "관리" -> "ì¼ê° ìƒíƒœ")' - text_journal_changed: "%{label}ì„(를) %{old}ì—서 %{new}(으)로 변경ë˜ì—ˆìŠµë‹ˆë‹¤." - text_journal_set_to: "%{label}ì„(를) %{value}(으)로 지정ë˜ì—ˆìŠµë‹ˆë‹¤." - text_journal_deleted: "%{label} ê°’ì´ ì§€ì›Œì¡ŒìŠµë‹ˆë‹¤. (%{old})" - label_group_plural: 그룹 - label_group: 그룹 - label_group_new: 새 그룹 - label_time_entry_plural: 작업시간 - text_journal_added: "%{label}ì— %{value}ì´(ê°€) 추가ë˜ì—ˆìŠµë‹ˆë‹¤." - field_active: 사용중 - enumeration_system_activity: 시스템 작업 - permission_delete_issue_watchers: ì¼ê°ì§€í‚´ì´ 지우기 - version_status_closed: 닫힘 - version_status_locked: ìž ê¹€ - version_status_open: ì§„í–‰ - error_can_not_reopen_issue_on_closed_version: 닫힌 ë²„ì „ì— í• ë‹¹ëœ ì¼ê°ì€ 다시 재발ìƒì‹œí‚¬ 수 없습니다. - label_user_anonymous: ì´ë¦„ì—†ìŒ - button_move_and_follow: ì´ë™í•˜ê³  ë”°ë¼ê°€ê¸° - setting_default_projects_modules: 새 프로ì íŠ¸ì— ê¸°ë³¸ì ìœ¼ë¡œ í™œì„±í™”ë  ëª¨ë“ˆ - setting_gravatar_default: 기본 ê·¸ë¼ë°”타 ì´ë¯¸ì§€ - field_sharing: 공유 - label_version_sharing_hierarchy: ìƒìœ„ ë° í•˜ìœ„ 프로ì íЏ - label_version_sharing_system: 모든 프로ì íЏ - label_version_sharing_descendants: 하위 프로ì íЏ - label_version_sharing_tree: 최ìƒìœ„ ë° ëª¨ë“  하위 프로ì íЏ - label_version_sharing_none: ê³µìœ ì—†ìŒ - error_can_not_archive_project: ì´ í”„ë¡œì íŠ¸ë¥¼ 잠금보관할 수 없습니다. - button_duplicate: 복제 - button_copy_and_follow: 복사하고 ë”°ë¼ê°€ê¸° - label_copy_source: ì›ë³¸ - setting_issue_done_ratio: ì¼ê°ì˜ ì§„ì²™ë„ ê³„ì‚°ë°©ë²• - setting_issue_done_ratio_issue_status: ì¼ê° ìƒíƒœë¥¼ 사용하기 - error_issue_done_ratios_not_updated: ì¼ê° ì§„ì²™ë„ê°€ 수정ë˜ì§€ 않았습니다. - error_workflow_copy_target: ëŒ€ìƒ ì¼ê°ì˜ 유형과 ì—­í• ì„ ì„ íƒí•˜ì„¸ìš”. - setting_issue_done_ratio_issue_field: ì¼ê° 수정ì—서 ì§„ì²™ë„ ìž…ë ¥í•˜ê¸° - label_copy_same_as_target: 대ìƒê³¼ ê°™ìŒ. - label_copy_target: ëŒ€ìƒ - notice_issue_done_ratios_updated: ì¼ê° ì§„ì²™ë„ê°€ 수정ë˜ì—ˆìŠµë‹ˆë‹¤. - error_workflow_copy_source: ì›ë³¸ ì¼ê°ì˜ 유형ì´ë‚˜ ì—­í• ì„ ì„ íƒí•˜ì„¸ìš”. - label_update_issue_done_ratios: 모든 ì¼ê° ì§„ì²™ë„ ê°±ì‹ í•˜ê¸° - setting_start_of_week: 달력 시작 ìš”ì¼ - permission_view_issues: ì¼ê° 보기 - label_display_used_statuses_only: ì´ ì¼ê° 유형ì—서 사용ë˜ëŠ” ìƒíƒœë§Œ 보여주기 - label_revision_id: ê°œì •íŒ %{value} - label_api_access_key: API 접근키 - label_api_access_key_created_on: API 접근키가 %{value} ì „ì— ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. - label_feeds_access_key: RSS 접근키 - notice_api_access_key_reseted: API 접근키가 초기화ë˜ì—ˆìŠµë‹ˆë‹¤. - setting_rest_api_enabled: REST 웹서비스 활성화 - label_missing_api_access_key: API 접근키가 없습니다. - label_missing_feeds_access_key: RSS 접근키가 없습니다. - button_show: 보기 - text_line_separated: 여러 ê°’ì´ í—ˆìš©ë¨(ê°’ 마다 한 줄씩) - setting_mail_handler_body_delimiters: ë©”ì¼ ë³¸ë¬¸ êµ¬ë¶„ìž - permission_add_subprojects: 하위 프로ì íЏ 만들기 - label_subproject_new: 새 하위 프로ì íЏ - text_own_membership_delete_confirmation: |- - 권한들 ì¼ë¶€ ë˜ëŠ” 전부를 막 삭제하려고 하고 있습니다. 그렇게 ë˜ë©´ ì´ í”„ë¡œì íŠ¸ë¥¼ ë”ì´ìƒ 수정할 수 없게 ë©ë‹ˆë‹¤. - 계ì†í•˜ì‹œê² ìŠµë‹ˆê¹Œ? - label_close_versions: ì™„ë£Œëœ ë²„ì „ 닫기 - label_board_sticky: ë¶™ë°•ì´ - label_board_locked: 잠금 - permission_export_wiki_pages: 위키 페ì´ì§€ 내보내기 - setting_cache_formatted_text: 형ì‹ì„ 가진 í…스트 빠른 임시 기억 - permission_manage_project_activities: 프로ì íЏ 작업내역 관리 - error_unable_delete_issue_status: ì¼ê° ìƒíƒœë¥¼ 지울 수 없습니다. - label_profile: 사용ìžì •ë³´ - permission_manage_subtasks: 하위 ì¼ê° 관리 - field_parent_issue: ìƒìœ„ ì¼ê° - label_subtask_plural: 하위 ì¼ê° - label_project_copy_notifications: 프로ì íЏ 복사 ì¤‘ì— ì´ë©”ì¼ ì•Œë¦¼ 보내기 - error_can_not_delete_custom_field: ì‚¬ìš©ìž ì •ì˜ í•„ë“œë¥¼ 삭제할 수 없습니다. - error_unable_to_connect: ì—°ê²°í•  수 없습니다((%{value}) - error_can_not_remove_role: ì´ ì—­í• ì€ í˜„ìž¬ 사용 중ì´ì´ì„œ 삭제할 수 없습니다. - error_can_not_delete_tracker: ì´ ìœ í˜•ì˜ ì¼ê°ë“¤ì´ 있어서 삭제할 수 없습니다. - field_principal: ì‹ ì› - label_my_page_block: ë‚´ 페ì´ì§€ 출력화면 - notice_failed_to_save_members: "%{errors}:구성ì›ì„ 저장 중 실패하였습니다" - text_zoom_out: ë” ìž‘ê²Œ - text_zoom_in: ë” í¬ê²Œ - notice_unable_delete_time_entry: 시간 ê¸°ë¡ í•­ëª©ì„ ì‚­ì œí•  수 없습니다. - label_overall_spent_time: ì´ ì†Œìš”ì‹œê°„ - field_time_entries: 기ë¡ëœ 시간 - project_module_gantt: Gantt 챠트 - project_module_calendar: 달력 - button_edit_associated_wikipage: "ì—°ê´€ëœ ìœ„í‚¤ 페ì´ì§€ %{page_title} 수정" - field_text: í…스트 ì˜ì—­ - label_user_mail_option_only_owner: ë‚´ê°€ ì €ìžì¸ 사항만 - setting_default_notification_option: 기본 알림 옵션 - label_user_mail_option_only_my_events: ë‚´ê°€ 지켜보거나 ì†í•´ìžˆëŠ” 사항만 - label_user_mail_option_only_assigned: ë‚´ì—게 í• ë‹¹ëœ ì‚¬í•­ë§Œ - label_user_mail_option_none: 알림 ì—†ìŒ - field_member_of_group: í• ë‹¹ëœ ì‚¬ëžŒì˜ ê·¸ë£¹ - field_assigned_to_role: í• ë‹¹ëœ ì‚¬ëžŒì˜ ì—­í•  - notice_not_authorized_archived_project: 접근하려는 프로ì íŠ¸ëŠ” ì´ë¯¸ 잠금보관ë˜ì–´ 있습니다. - label_principal_search: "ì‚¬ìš©ìž ë° ê·¸ë£¹ 찾기:" - label_user_search: "ì‚¬ìš©ìž ì°¾ê¸°::" - field_visible: ë³´ì´ê¸° - setting_emails_header: ì´ë©”ì¼ í—¤ë” - setting_commit_logtime_activity_id: 기ë¡ëœ ì‹œê°„ì— ì ìš©í•  작업분류 - text_time_logged_by_changeset: "ë³€ê²½ë¬¶ìŒ %{value}ì—서 ì ìš©ë˜ì—ˆìŠµë‹ˆë‹¤." - setting_commit_logtime_enabled: 커밋 시ì ì— 작업 시간 ê¸°ë¡ í™œì„±í™” - notice_gantt_chart_truncated: "표시할 수 있는 최대 항목수(%{max})를 초과하여 차트가 잘렸습니다." - setting_gantt_items_limit: "Gantt ì°¨íŠ¸ì— í‘œì‹œë˜ëŠ” 최대 항목수" - 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: "커밋(commit) ê¸°ë¡ ì¸ì½”딩" - field_scm_path_encoding: "경로 ì¸ì½”딩" - text_scm_path_encoding_note: "기본: UTF-8" - field_path_to_repository: "저장소 경로" - field_root_directory: "루트 경로" - field_cvs_module: "모듈" - field_cvsroot: "CVS 루트" - text_mercurial_repository_note: "로컬 저장소 (예: /hgrepo, c:\\hgrepo)" - text_scm_command: "명령" - text_scm_command_version: "버전" - label_git_report_last_commit: "파ì¼ì´ë‚˜ í´ë”ì˜ ë§ˆì§€ë§‰ 커밋(commit)ì„ ë³´ê³ " - text_scm_config: "SCM ëª…ë ¹ì„ config/configuration.ymlì—서 수정할 수 있습니다. 수정후ì—는 재시작하십시오." - text_scm_command_not_available: "SCM ëª…ë ¹ì„ ì‚¬ìš©í•  수 없습니다. 관리 페ì´ì§€ì˜ ì„¤ì •ì„ ê²€ì‚¬í•˜ì‹­ì‹œì˜¤." - notice_issue_successful_create: "%{id} ì¼ê°ì´ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤." - label_between: "사ì´" - setting_issue_group_assignment: "ê·¸ë£¹ì— ì¼ê° 할당 허용" - label_diff: "비êµ(diff)" - text_git_repository_note: "ë¡œì»¬ì˜ bare 저장소 (예: /gitrepo, c:\\gitrepo)" - 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),숫ìž,대쉬(-)와 밑줄(_)ë§Œ 가능합니다.
ì‹ë³„ìžëŠ” 저장후ì—는 수정할 수 없습니다." - field_multiple: 복수선íƒê°€ëŠ¥ - setting_commit_cross_project_ref: 다른 프로ì íŠ¸ì˜ ì¼ê° 참조 ë° ìˆ˜ì • 허용 - text_issue_conflict_resolution_add_notes: ë³€ê²½ë‚´ìš©ì€ ì·¨ì†Œí•˜ê³  ë§ê¸€ë§Œ 추가 - text_issue_conflict_resolution_overwrite: 변경내용 ê°•ì œì ìš© (ì´ì „ ë§ê¸€ì„ 제외하고 ë®ì–´ ì”니다) - notice_issue_update_conflict: ì¼ê°ì´ 수정ë˜ëŠ” ë™ì•ˆ 다른 사용ìžì— ì˜í•´ì„œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. - text_issue_conflict_resolution_cancel: "ë³€ê²½ë‚´ìš©ì„ ë˜ëŒë¦¬ê³  다시 표시 %{link}" - permission_manage_related_issues: ì—°ê²°ëœ ì¼ê° 관리 - field_auth_source_ldap_filter: LDAP í•„í„° - label_search_for_watchers: 추가할 ì¼ê°ì§€í‚´ì´ 검색 - notice_account_deleted: ë‹¹ì‹ ì˜ ê³„ì •ì´ ì™„ì „ížˆ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤. - setting_unsubscribe: 사용ìžë“¤ì´ ìžì‹ ì˜ ê³„ì •ì„ ì‚­ì œí† ë¡ í—ˆìš© - button_delete_my_account: ë‚˜ì˜ ê³„ì • ì‚­ì œ - text_account_destroy_confirmation: |- - 계ì†í•˜ì‹œê² ìŠµë‹ˆê¹Œ? - ê³„ì •ì´ ì‚­ì œë˜ë©´ 복구할 수 없습니다. - error_session_expired: ë‹¹ì‹ ì˜ ì„¸ì…˜ì´ ë§Œë£Œë˜ì—ˆìŠµë‹ˆë‹¤. 다시 로그ì¸í•˜ì„¸ìš”. - text_session_expiration_settings: "경고: ì´ ì„¤ì •ì„ ë°”ê¾¸ë©´ ë‹¹ì‹ ì„ í¬í•¨í•˜ì—¬ í˜„ìž¬ì˜ ì„¸ì…˜ë“¤ì„ ë§Œë£Œì‹œí‚¬ 수 있습니다." - setting_session_lifetime: 세션 최대 시간 - setting_session_timeout: 세션 비활성화 타임아웃 - label_session_expiration: 세션 만료 - permission_close_project: 프로ì íŠ¸ë¥¼ 닫거나 다시 열기 - label_show_closed_projects: 닫힌 프로ì íЏ 보기 - button_close: 닫기 - button_reopen: 다시 열기 - project_status_active: 사용중 - project_status_closed: 닫힘 - project_status_archived: 잠금보관 - text_project_closed: ì´ í”„ë¡œì íŠ¸ëŠ” 닫혀 있으며 ì½ê¸° 전용입니다. - notice_user_successful_create: ì‚¬ìš©ìž %{id} ì´(ê°€) ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. - field_core_fields: 표준 항목들 - field_timeout: 타임아웃 (ì´ˆ) - setting_thumbnails_enabled: 첨부파ì¼ì˜ ì¸ë„¤ì¼ì„ 보여줌 - setting_thumbnails_size: ì¸ë„¤ì¼ í¬ê¸° (픽셀) - label_status_transitions: ì¼ê° ìƒíƒœ 변경 - label_fields_permissions: 항목 편집 권한 - label_readonly: ì½ê¸° ì „ìš© - label_required: 필수 - text_repository_identifier_info: "소문ìž(a-z),숫ìž,대쉬(-)와 밑줄(_)ë§Œ 가능합니다.
ì‹ë³„ìžëŠ” 저장후ì—는 수정할 수 없습니다." - field_board_parent: Parent forum - label_attribute_of_project: "프로ì íŠ¸ì˜ %{name}" - label_attribute_of_author: "ì €ìžì˜ %{name}" - label_attribute_of_assigned_to: "담당ìžì˜ %{name}" - label_attribute_of_fixed_version: "ëª©í‘œë²„ì „ì˜ %{name}" - label_copy_subtasks: 하위 ì¼ê°ë“¤ì„ 복사 - label_copied_to: "ë‹¤ìŒ ì¼ê°ìœ¼ë¡œ 복사ë¨:" - label_copied_from: "ë‹¤ìŒ ì¼ê°ìœ¼ë¡œë¶€í„° 복사ë¨:" - label_any_issues_in_project: ë‹¤ìŒ í”„ë¡œì íŠ¸ì— ì†í•œ 아무 ì¼ê° - label_any_issues_not_in_project: ë‹¤ìŒ í”„ë¡œì íŠ¸ì— ì†í•˜ì§€ ì•Šì€ ì•„ë¬´ ì¼ê° - field_private_notes: 비공개 ë§ê¸€ - permission_view_private_notes: 비공개 ë§ê¸€ 보기 - permission_set_notes_private: ë§ê¸€ì„ 비공개로 설정 - label_no_issues_in_project: ë‹¤ìŒ í”„ë¡œì íЏ ë‚´ì—서 해당 ì¼ê° ì—†ìŒ - label_any: ëª¨ë‘ - label_last_n_weeks: 최근 %{count} 주 - setting_cross_project_subtasks: 다른 프로ì íŠ¸ì˜ ì¼ê°ì„ ìƒìœ„ ì¼ê°ìœ¼ë¡œ 지정하는 ê²ƒì„ í—ˆìš© - label_cross_project_descendants: 하위 프로ì íЏ - label_cross_project_tree: 최ìƒìœ„ ë° ëª¨ë“  하위 프로ì íЏ - label_cross_project_hierarchy: ìƒìœ„ ë° í•˜ìœ„ 프로ì íЏ - label_cross_project_system: 모든 프로ì íЏ - button_hide: 숨기기 - setting_non_working_week_days: ë¹„ê·¼ë¬´ì¼ (non-working days) - label_in_the_next_days: ë‹¤ìŒ - label_in_the_past_days: 지난 diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/04353faa53c81964d7239a9938818ad1596cb331.svn-base --- a/.svn/pristine/04/04353faa53c81964d7239a9938818ad1596cb331.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,218 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'messages_controller' - -# Re-raise errors caught by the controller. -class MessagesController; def rescue_action(e) raise e end; end - -class MessagesControllerTest < ActionController::TestCase - fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules - - def setup - @controller = MessagesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - end - - def test_show - get :show, :board_id => 1, :id => 1 - assert_response :success - assert_template 'show' - assert_not_nil assigns(:board) - assert_not_nil assigns(:project) - assert_not_nil assigns(:topic) - end - - def test_show_should_contain_reply_field_tags_for_quoting - @request.session[:user_id] = 2 - get :show, :board_id => 1, :id => 1 - assert_response :success - - # tags required by MessagesController#quote - assert_tag 'input', :attributes => {:id => 'message_subject'} - assert_tag 'textarea', :attributes => {:id => 'message_content'} - assert_tag 'div', :attributes => {:id => 'reply'} - end - - def test_show_with_pagination - message = Message.find(1) - assert_difference 'Message.count', 30 do - 30.times do - message.children << Message.new(:subject => 'Reply', :content => 'Reply body', :author_id => 2, :board_id => 1) - end - end - get :show, :board_id => 1, :id => 1, :r => message.children.last(:order => 'id').id - assert_response :success - assert_template 'show' - replies = assigns(:replies) - assert_not_nil replies - assert !replies.include?(message.children.first(:order => 'id')) - assert replies.include?(message.children.last(:order => 'id')) - end - - def test_show_with_reply_permission - @request.session[:user_id] = 2 - get :show, :board_id => 1, :id => 1 - assert_response :success - assert_template 'show' - assert_tag :div, :attributes => { :id => 'reply' }, - :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } } - end - - def test_show_message_not_found - get :show, :board_id => 1, :id => 99999 - assert_response 404 - end - - def test_show_message_from_invalid_board_should_respond_with_404 - get :show, :board_id => 999, :id => 1 - assert_response 404 - end - - def test_get_new - @request.session[:user_id] = 2 - get :new, :board_id => 1 - assert_response :success - assert_template 'new' - end - - def test_post_new - @request.session[:user_id] = 2 - ActionMailer::Base.deliveries.clear - - with_settings :notified_events => %w(message_posted) do - post :new, :board_id => 1, - :message => { :subject => 'Test created message', - :content => 'Message body'} - end - message = Message.find_by_subject('Test created message') - assert_not_nil message - assert_redirected_to "/boards/1/topics/#{message.to_param}" - assert_equal 'Message body', message.content - assert_equal 2, message.author_id - assert_equal 1, message.board_id - - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject - assert_mail_body_match 'Message body', mail - # author - assert mail.bcc.include?('jsmith@somenet.foo') - # project member - assert mail.bcc.include?('dlopper@somenet.foo') - end - - def test_get_edit - @request.session[:user_id] = 2 - get :edit, :board_id => 1, :id => 1 - assert_response :success - assert_template 'edit' - end - - def test_post_edit - @request.session[:user_id] = 2 - post :edit, :board_id => 1, :id => 1, - :message => { :subject => 'New subject', - :content => 'New body'} - assert_redirected_to '/boards/1/topics/1' - message = Message.find(1) - assert_equal 'New subject', message.subject - assert_equal 'New body', message.content - end - - def test_post_edit_sticky_and_locked - @request.session[:user_id] = 2 - post :edit, :board_id => 1, :id => 1, - :message => { :subject => 'New subject', - :content => 'New body', - :locked => '1', - :sticky => '1'} - assert_redirected_to '/boards/1/topics/1' - message = Message.find(1) - assert_equal true, message.sticky? - assert_equal true, message.locked? - end - - def test_post_edit_should_allow_to_change_board - @request.session[:user_id] = 2 - post :edit, :board_id => 1, :id => 1, - :message => { :subject => 'New subject', - :content => 'New body', - :board_id => 2} - assert_redirected_to '/boards/2/topics/1' - message = Message.find(1) - assert_equal Board.find(2), message.board - end - - def test_reply - @request.session[:user_id] = 2 - post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' } - reply = Message.find(:first, :order => 'id DESC') - assert_redirected_to "/boards/1/topics/1?r=#{reply.id}" - assert Message.find_by_subject('Test reply') - end - - def test_destroy_topic - @request.session[:user_id] = 2 - assert_difference 'Message.count', -3 do - post :destroy, :board_id => 1, :id => 1 - end - assert_redirected_to '/projects/ecookbook/boards/1' - assert_nil Message.find_by_id(1) - end - - def test_destroy_reply - @request.session[:user_id] = 2 - assert_difference 'Message.count', -1 do - post :destroy, :board_id => 1, :id => 2 - end - assert_redirected_to '/boards/1/topics/1?r=2' - assert_nil Message.find_by_id(2) - end - - def test_quote - @request.session[:user_id] = 2 - xhr :get, :quote, :board_id => 1, :id => 3 - assert_response :success - assert_equal 'text/javascript', response.content_type - assert_template 'quote' - assert_include 'RE: First post', response.body - assert_include '> An other reply', response.body - end - - def test_preview_new - @request.session[:user_id] = 2 - post :preview, - :board_id => 1, - :message => {:subject => "", :content => "Previewed text"} - assert_response :success - assert_template 'common/_preview' - end - - def test_preview_edit - @request.session[:user_id] = 2 - post :preview, - :id => 4, - :board_id => 1, - :message => {:subject => "", :content => "Previewed text"} - assert_response :success - assert_template 'common/_preview' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/046cbe787072732ca9cd18ffb9fce1aa135f723e.svn-base --- a/.svn/pristine/04/046cbe787072732ca9cd18ffb9fce1aa135f723e.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -

<%= l(@enumeration.option_name) %>: <%=h @enumeration %>

- -<%= form_tag({}, :method => :delete) do %> -
-

<%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %>

-

-<%= select_tag 'reassign_to_id', (content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options_from_collection_for_select(@enumerations, 'id', 'name')) %>

-
- -<%= submit_tag l(:button_apply) %> -<%= link_to l(:button_cancel), enumerations_path %> -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/046d428ac69934d1d2d0ae1a160753bb78acb462.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/046d428ac69934d1d2d0ae1a160753bb78acb462.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,304 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldTest < ActiveSupport::TestCase + fixtures :custom_fields + + def test_create + field = UserCustomField.new(:name => 'Money money money', :field_format => 'float') + assert field.save + end + + def test_before_validation + field = CustomField.new(:name => 'test_before_validation', :field_format => 'int') + field.searchable = true + assert field.save + assert_equal false, field.searchable + field.searchable = true + assert field.save + assert_equal false, field.searchable + end + + def test_regexp_validation + field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9') + assert !field.save + assert_include I18n.t('activerecord.errors.messages.invalid'), + field.errors[:regexp] + field.regexp = '[a-z0-9]' + assert field.save + end + + def test_default_value_should_be_validated + field = CustomField.new(:name => 'Test', :field_format => 'int') + field.default_value = 'abc' + assert !field.valid? + field.default_value = '6' + assert field.valid? + end + + def test_default_value_should_not_be_validated_when_blank + field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '') + assert field.valid? + end + + def test_field_format_should_be_validated + field = CustomField.new(:name => 'Test', :field_format => 'foo') + assert !field.valid? + end + + def test_field_format_validation_should_accept_formats_added_at_runtime + Redmine::CustomFieldFormat.register 'foobar' + + field = CustomField.new(:name => 'Some Custom Field', :field_format => 'foobar') + assert field.valid?, 'field should be valid' + ensure + Redmine::CustomFieldFormat.delete 'foobar' + end + + def test_should_not_change_field_format_of_existing_custom_field + field = CustomField.find(1) + field.field_format = 'int' + assert_equal 'list', field.field_format + end + + def test_possible_values_should_accept_an_array + field = CustomField.new + field.possible_values = ["One value", ""] + assert_equal ["One value"], field.possible_values + end + + def test_possible_values_should_accept_a_string + field = CustomField.new + field.possible_values = "One value" + assert_equal ["One value"], field.possible_values + end + + def test_possible_values_should_accept_a_multiline_string + field = CustomField.new + field.possible_values = "One value\nAnd another one \r\n \n" + assert_equal ["One value", "And another one"], field.possible_values + end + + if "string".respond_to?(:encoding) + def test_possible_values_stored_as_binary_should_be_utf8_encoded + field = CustomField.find(11) + assert_kind_of Array, field.possible_values + assert field.possible_values.size > 0 + field.possible_values.each do |value| + assert_equal "UTF-8", value.encoding.name + end + end + end + + def test_destroy + field = CustomField.find(1) + assert field.destroy + end + + def test_new_subclass_instance_should_return_an_instance + f = CustomField.new_subclass_instance('IssueCustomField') + assert_kind_of IssueCustomField, f + end + + def test_new_subclass_instance_should_set_attributes + f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test') + assert_kind_of IssueCustomField, f + assert_equal 'Test', f.name + end + + def test_new_subclass_instance_with_invalid_class_name_should_return_nil + assert_nil CustomField.new_subclass_instance('WrongClassName') + end + + def test_new_subclass_instance_with_non_subclass_name_should_return_nil + assert_nil CustomField.new_subclass_instance('Project') + end + + def test_string_field_validation_with_blank_value + f = CustomField.new(:field_format => 'string') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + + f.is_required = true + assert !f.valid_field_value?(nil) + assert !f.valid_field_value?('') + end + + def test_string_field_validation_with_min_and_max_lengths + f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5) + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?('a' * 2) + assert !f.valid_field_value?('a') + assert !f.valid_field_value?('a' * 6) + end + + def test_string_field_validation_with_regexp + f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?('ABC') + assert !f.valid_field_value?('abc') + end + + def test_date_field_validation + f = CustomField.new(:field_format => 'date') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?('1975-07-14') + assert !f.valid_field_value?('1975-07-33') + assert !f.valid_field_value?('abc') + end + + def test_list_field_validation + f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2']) + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?('value2') + assert !f.valid_field_value?('abc') + end + + def test_int_field_validation + f = CustomField.new(:field_format => 'int') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?('123') + assert f.valid_field_value?('+123') + assert f.valid_field_value?('-123') + assert !f.valid_field_value?('6abc') + end + + def test_float_field_validation + f = CustomField.new(:field_format => 'float') + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?('11.2') + assert f.valid_field_value?('-6.250') + assert f.valid_field_value?('5') + assert !f.valid_field_value?('6abc') + end + + def test_multi_field_validation + f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2']) + + assert f.valid_field_value?(nil) + assert f.valid_field_value?('') + assert !f.valid_field_value?(' ') + assert f.valid_field_value?([]) + assert f.valid_field_value?([nil]) + assert f.valid_field_value?(['']) + assert !f.valid_field_value?([' ']) + + assert f.valid_field_value?('value2') + assert !f.valid_field_value?('abc') + + assert f.valid_field_value?(['value2']) + assert !f.valid_field_value?(['abc']) + + assert f.valid_field_value?(['', 'value2']) + assert !f.valid_field_value?(['', 'abc']) + + assert f.valid_field_value?(['value1', 'value2']) + assert !f.valid_field_value?(['value1', 'abc']) + end + + def test_changing_multiple_to_false_should_delete_multiple_values + field = ProjectCustomField.create!(:name => 'field', :field_format => 'list', :multiple => 'true', :possible_values => ['field1', 'field2']) + other = ProjectCustomField.create!(:name => 'other', :field_format => 'list', :multiple => 'true', :possible_values => ['other1', 'other2']) + + item_with_multiple_values = Project.generate!(:custom_field_values => {field.id => ['field1', 'field2'], other.id => ['other1', 'other2']}) + item_with_single_values = Project.generate!(:custom_field_values => {field.id => ['field1'], other.id => ['other2']}) + + assert_difference 'CustomValue.count', -1 do + field.multiple = false + field.save! + end + + item_with_multiple_values = Project.find(item_with_multiple_values.id) + assert_kind_of String, item_with_multiple_values.custom_field_value(field) + assert_kind_of Array, item_with_multiple_values.custom_field_value(other) + assert_equal 2, item_with_multiple_values.custom_field_value(other).size + end + + def test_value_class_should_return_the_class_used_for_fields_values + assert_equal User, CustomField.new(:field_format => 'user').value_class + assert_equal Version, CustomField.new(:field_format => 'version').value_class + end + + def test_value_class_should_return_nil_for_other_fields + assert_nil CustomField.new(:field_format => 'text').value_class + assert_nil CustomField.new.value_class + end + + def test_value_from_keyword_for_list_custom_field + field = CustomField.find(1) + assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1)) + end + + def test_visibile_scope_with_admin_should_return_all_custom_fields + CustomField.delete_all + fields = [ + CustomField.generate!(:visible => true), + CustomField.generate!(:visible => false), + CustomField.generate!(:visible => false, :role_ids => [1, 3]), + CustomField.generate!(:visible => false, :role_ids => [1, 2]), + ] + + assert_equal 4, CustomField.visible(User.find(1)).count + end + + def test_visibile_scope_with_non_admin_user_should_return_visible_custom_fields + CustomField.delete_all + fields = [ + CustomField.generate!(:visible => true), + CustomField.generate!(:visible => false), + CustomField.generate!(:visible => false, :role_ids => [1, 3]), + CustomField.generate!(:visible => false, :role_ids => [1, 2]), + ] + user = User.generate! + User.add_to_project(user, Project.first, Role.find(3)) + + assert_equal [fields[0], fields[2]], CustomField.visible(user).order("id").to_a + end + + def test_visibile_scope_with_anonymous_user_should_return_visible_custom_fields + CustomField.delete_all + fields = [ + CustomField.generate!(:visible => true), + CustomField.generate!(:visible => false), + CustomField.generate!(:visible => false, :role_ids => [1, 3]), + CustomField.generate!(:visible => false, :role_ids => [1, 2]), + ] + + assert_equal [fields[0]], CustomField.visible(User.anonymous).order("id").to_a + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/046e745fdf9ac7fc7dec5d1b9e47c29909c11738.svn-base --- a/.svn/pristine/04/046e745fdf9ac7fc7dec5d1b9e47c29909c11738.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -.jstEditor { - padding-left: 0px; -} -.jstEditor textarea, .jstEditor iframe { - margin: 0; -} - -.jstHandle { - height: 10px; - font-size: 0.1em; - cursor: s-resize; - /*background: transparent url(img/resizer.png) no-repeat 45% 50%;*/ -} - -.jstElements { - padding: 3px 3px 3px 0; -} - -.jstElements button { - margin-right: 4px; - width : 24px; - height: 24px; - padding: 4px; - border-style: solid; - border-width: 1px; - border-color: #ddd; - background-color : #f7f7f7; - background-position : 50% 50%; - background-repeat: no-repeat; -} -.jstElements button:hover { - border-color: #bbb; - background-color: #e5e5e5; -} -.jstElements button span { - display : none; -} -.jstElements span { - display : inline; -} - -.jstSpacer { - width : 0px; - font-size: 1px; - margin-right: 6px; -} - -.jstElements .help { float: right; margin-right: 0.5em; padding-top: 8px; font-size: 0.9em; } -.jstElements .help a {padding: 2px 0 2px 20px; background: url(../images/help.png) no-repeat 0 50%;} - -/* Buttons --------------------------------------------------------- */ -.jstb_strong { - background-image: url(../images/jstoolbar/bt_strong.png); -} -.jstb_em { - background-image: url(../images/jstoolbar/bt_em.png); -} -.jstb_ins { - background-image: url(../images/jstoolbar/bt_ins.png); -} -.jstb_del { - background-image: url(../images/jstoolbar/bt_del.png); -} -.jstb_code { - background-image: url(../images/jstoolbar/bt_code.png); -} -.jstb_h1 { - background-image: url(../images/jstoolbar/bt_h1.png); -} -.jstb_h2 { - background-image: url(../images/jstoolbar/bt_h2.png); -} -.jstb_h3 { - background-image: url(../images/jstoolbar/bt_h3.png); -} -.jstb_ul { - background-image: url(../images/jstoolbar/bt_ul.png); -} -.jstb_ol { - background-image: url(../images/jstoolbar/bt_ol.png); -} -.jstb_bq { - background-image: url(../images/jstoolbar/bt_bq.png); -} -.jstb_unbq { - background-image: url(../images/jstoolbar/bt_bq_remove.png); -} -.jstb_pre { - background-image: url(../images/jstoolbar/bt_pre.png); -} -.jstb_link { - background-image: url(../images/jstoolbar/bt_link.png); -} -.jstb_img { - background-image: url(../images/jstoolbar/bt_img.png); -} diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/048c8c7c3fc4fac08662cbdaac59cd6348bb3d14.svn-base --- a/.svn/pristine/04/048c8c7c3fc4fac08662cbdaac59cd6348bb3d14.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1287 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2012 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 'forwardable' -require 'cgi' - -module ApplicationHelper - include Redmine::WikiFormatting::Macros::Definitions - include Redmine::I18n - include GravatarHelper::PublicMethods - - extend Forwardable - def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter - - # Return true if user is authorized for controller/action, otherwise false - def authorize_for(controller, action) - User.current.allowed_to?({:controller => controller, :action => action}, @project) - end - - # Display a link if user is authorized - # - # @param [String] name Anchor text (passed to link_to) - # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized - # @param [optional, Hash] html_options Options passed to link_to - # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to - def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) - link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) - end - - # Displays a link to user's account page if active - def link_to_user(user, options={}) - if user.is_a?(User) - name = h(user.name(options[:format])) - if user.active? || (User.current.admin? && user.logged?) - link_to name, user_path(user), :class => user.css_classes - else - name - end - else - h(user.to_s) - end - end - - # Displays a link to +issue+ with its subject. - # Examples: - # - # link_to_issue(issue) # => Defect #6: This is the subject - # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... - # link_to_issue(issue, :subject => false) # => Defect #6 - # link_to_issue(issue, :project => true) # => Foo - Defect #6 - # link_to_issue(issue, :subject => false, :tracker => false) # => #6 - # - def link_to_issue(issue, options={}) - title = nil - subject = nil - text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" - if options[:subject] == false - title = truncate(issue.subject, :length => 60) - else - subject = issue.subject - if options[:truncate] - subject = truncate(subject, :length => options[:truncate]) - end - end - s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title - s << h(": #{subject}") if subject - s = h("#{issue.project} - ") + s if options[:project] - s - end - - # Generates a link to an attachment. - # Options: - # * :text - Link text (default to attachment filename) - # * :download - Force download (default: false) - def link_to_attachment(attachment, options={}) - text = options.delete(:text) || attachment.filename - action = options.delete(:download) ? 'download' : 'show' - opt_only_path = {} - opt_only_path[:only_path] = (options[:only_path] == false ? false : true) - options.delete(:only_path) - link_to(h(text), - {:controller => 'attachments', :action => action, - :id => attachment, :filename => attachment.filename}.merge(opt_only_path), - options) - end - - # Generates a link to a SCM revision - # Options: - # * :text - Link text (default to the formatted revision) - def link_to_revision(revision, repository, options={}) - if repository.is_a?(Project) - repository = repository.repository - end - text = options.delete(:text) || format_revision(revision) - rev = revision.respond_to?(:identifier) ? revision.identifier : revision - link_to( - h(text), - {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, - :title => l(:label_revision_id, format_revision(revision)) - ) - end - - # Generates a link to a message - def link_to_message(message, options={}, html_options = nil) - link_to( - h(truncate(message.subject, :length => 60)), - { :controller => 'messages', :action => 'show', - :board_id => message.board_id, - :id => (message.parent_id || message.id), - :r => (message.parent_id && message.id), - :anchor => (message.parent_id ? "message-#{message.id}" : nil) - }.merge(options), - html_options - ) - end - - # Generates a link to a project if active - # Examples: - # - # link_to_project(project) # => link to the specified project overview - # link_to_project(project, :action=>'settings') # => link to project settings - # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options - # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) - # - def link_to_project(project, options={}, html_options = nil) - if project.archived? - h(project) - else - url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) - link_to(h(project), url, html_options) - end - end - - def wiki_page_path(page, options={}) - url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) - end - - def thumbnail_tag(attachment) - link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), - {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, - :title => attachment.filename - end - - def toggle_link(name, id, options={}) - onclick = "$('##{id}').toggle(); " - onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") - onclick << "return false;" - link_to(name, "#", :onclick => onclick) - end - - def image_to_function(name, function, html_options = {}) - html_options.symbolize_keys! - tag(:input, html_options.merge({ - :type => "image", :src => image_path(name), - :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" - })) - end - - def format_activity_title(text) - h(truncate_single_line(text, :length => 100)) - end - - def format_activity_day(date) - date == User.current.today ? l(:label_today).titleize : format_date(date) - end - - def format_activity_description(text) - h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') - ).gsub(/[\r\n]+/, "
").html_safe - end - - def format_version_name(version) - if version.project == @project - h(version) - else - h("#{version.project} - #{version}") - end - end - - def due_date_distance_in_words(date) - if date - l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) - end - end - - # Renders a tree of projects as a nested set of unordered lists - # The given collection may be a subset of the whole project tree - # (eg. some intermediate nodes are private and can not be seen) - def render_project_nested_lists(projects) - s = '' - if projects.any? - ancestors = [] - original_project = @project - projects.sort_by(&:lft).each do |project| - # set the project environment to please macros. - @project = project - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "
    \n" - else - ancestors.pop - s << "" - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - s << "
\n" - end - end - classes = (ancestors.empty? ? 'root' : 'child') - s << "
  • " - s << h(block_given? ? yield(project) : project.name) - s << "
    \n" - ancestors << project - end - s << ("
  • \n" * ancestors.size) - @project = original_project - end - s.html_safe - end - - def render_page_hierarchy(pages, node=nil, options={}) - content = '' - if pages[node] - content << "
      \n" - pages[node].each do |page| - content << "
    • " - content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}, - :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) - content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] - content << "
    • \n" - end - content << "
    \n" - end - content.html_safe - end - - # Renders flash messages - def render_flash_messages - s = '' - flash.each do |k,v| - s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") - end - s.html_safe - end - - # Renders tabs and their content - def render_tabs(tabs) - if tabs.any? - render :partial => 'common/tabs', :locals => {:tabs => tabs} - else - content_tag 'p', l(:label_no_data), :class => "nodata" - end - end - - # Renders the project quick-jump box - def render_project_jump_box - return unless User.current.logged? - projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq - if projects.any? - options = - ("" + - '').html_safe - - options << project_tree_options_for_select(projects, :selected => @project) do |p| - { :value => project_path(:id => p, :jump => current_menu_item) } - end - - select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') - end - end - - def project_tree_options_for_select(projects, options = {}) - s = '' - project_tree(projects) do |project, level| - name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe - tag_options = {:value => project.id} - if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) - tag_options[:selected] = 'selected' - else - tag_options[:selected] = nil - end - tag_options.merge!(yield(project)) if block_given? - s << content_tag('option', name_prefix + h(project), tag_options) - end - s.html_safe - end - - # Yields the given block for each project with its level in the tree - # - # Wrapper for Project#project_tree - def project_tree(projects, &block) - Project.project_tree(projects, &block) - end - - def principals_check_box_tags(name, principals) - s = '' - principals.sort.each do |principal| - s << "\n" - end - s.html_safe - end - - # Returns a string for users/groups option tags - def principals_options_for_select(collection, selected=nil) - s = '' - if collection.include?(User.current) - s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) - end - groups = '' - collection.sort.each do |element| - selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) - (element.is_a?(Group) ? groups : s) << %() - end - unless groups.empty? - s << %(#{groups}) - end - s.html_safe - end - - # Options for the new membership projects combo-box - def options_for_membership_project_select(principal, projects) - options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") - options << project_tree_options_for_select(projects) do |p| - {:disabled => principal.projects.include?(p)} - end - options - end - - # Truncates and returns the string as a single line - def truncate_single_line(string, *args) - truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') - end - - # Truncates at line break after 250 characters or options[:length] - def truncate_lines(string, options={}) - length = options[:length] || 250 - if string.to_s =~ /\A(.{#{length}}.*?)$/m - "#{$1}..." - else - string - end - end - - def anchor(text) - text.to_s.gsub(' ', '_') - end - - def html_hours(text) - text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe - end - - def authoring(created, author, options={}) - l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe - end - - def time_tag(time) - text = distance_of_time_in_words(Time.now, time) - if @project - link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) - else - content_tag('acronym', text, :title => format_time(time)) - end - end - - def syntax_highlight_lines(name, content) - lines = [] - syntax_highlight(name, content).each_line { |line| lines << line } - lines - end - - def syntax_highlight(name, content) - Redmine::SyntaxHighlighting.highlight_by_filename(content, name) - end - - def to_path_param(path) - str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") - str.blank? ? nil : str - end - - def pagination_links_full(paginator, count=nil, options={}) - page_param = options.delete(:page_param) || :page - per_page_links = options.delete(:per_page_links) - url_param = params.dup - - html = '' - if paginator.current.previous - # \xc2\xab(utf-8) = « - html << link_to_content_update( - "\xc2\xab " + l(:label_previous), - url_param.merge(page_param => paginator.current.previous)) + ' ' - end - - html << (pagination_links_each(paginator, options) do |n| - link_to_content_update(n.to_s, url_param.merge(page_param => n)) - end || '') - - if paginator.current.next - # \xc2\xbb(utf-8) = » - html << ' ' + link_to_content_update( - (l(:label_next) + " \xc2\xbb"), - url_param.merge(page_param => paginator.current.next)) - end - - unless count.nil? - html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" - if per_page_links != false && links = per_page_links(paginator.items_per_page, count) - html << " | #{links}" - end - end - - html.html_safe - end - - def per_page_links(selected=nil, item_count=nil) - values = Setting.per_page_options_array - if item_count && values.any? - if item_count > values.first - max = values.detect {|value| value >= item_count} || item_count - else - max = item_count - end - values = values.select {|value| value <= max || value == selected} - end - if values.empty? || (values.size == 1 && values.first == selected) - return nil - end - links = values.collect do |n| - n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) - end - l(:label_display_per_page, links.join(', ')) - end - - def reorder_links(name, url, method = :post) - link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), - url.merge({"#{name}[move_to]" => 'highest'}), - :method => method, :title => l(:label_sort_highest)) + - link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), - url.merge({"#{name}[move_to]" => 'higher'}), - :method => method, :title => l(:label_sort_higher)) + - link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), - url.merge({"#{name}[move_to]" => 'lower'}), - :method => method, :title => l(:label_sort_lower)) + - link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), - url.merge({"#{name}[move_to]" => 'lowest'}), - :method => method, :title => l(:label_sort_lowest)) - end - - def breadcrumb(*args) - elements = args.flatten - elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil - end - - def other_formats_links(&block) - concat('

    '.html_safe + l(:label_export_to)) - yield Redmine::Views::OtherFormatsBuilder.new(self) - concat('

    '.html_safe) - end - - def page_header_title - if @project.nil? || @project.new_record? - h(Setting.app_title) - else - b = [] - ancestors = (@project.root? ? [] : @project.ancestors.visible.all) - if ancestors.any? - root = ancestors.shift - b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') - if ancestors.size > 2 - b << "\xe2\x80\xa6" - ancestors = ancestors[-2, 2] - end - b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } - end - b << h(@project) - b.join(" \xc2\xbb ").html_safe - end - end - - def html_title(*args) - if args.empty? - title = @html_title || [] - title << @project.name if @project - title << Setting.app_title unless Setting.app_title == title.last - title.select {|t| !t.blank? }.join(' - ') - else - @html_title ||= [] - @html_title += args - end - end - - # Returns the theme, controller name, and action as css classes for the - # HTML body. - def body_css_classes - css = [] - if theme = Redmine::Themes.theme(Setting.ui_theme) - css << 'theme-' + theme.name - end - - css << 'controller-' + controller_name - css << 'action-' + action_name - css.join(' ') - end - - def accesskey(s) - Redmine::AccessKeys.key_for s - end - - # Formats text according to system settings. - # 2 ways to call this method: - # * with a String: textilizable(text, options) - # * with an object and one of its attribute: textilizable(issue, :description, options) - def textilizable(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - case args.size - when 1 - obj = options[:object] - text = args.shift - when 2 - obj = args.shift - attr = args.shift - text = obj.send(attr).to_s - else - raise ArgumentError, 'invalid arguments to textilizable' - end - return '' if text.blank? - project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) - only_path = options.delete(:only_path) == false ? false : true - - text = text.dup - macros = catch_macros(text) - text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) - - @parsed_headings = [] - @heading_anchors = {} - @current_section = 0 if options[:edit_section_links] - - parse_sections(text, project, obj, attr, only_path, options) - text = parse_non_pre_blocks(text, obj, macros) do |text| - [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| - send method_name, text, project, obj, attr, only_path, options - end - end - parse_headings(text, project, obj, attr, only_path, options) - - if @parsed_headings.any? - replace_toc(text, @parsed_headings) - end - - text.html_safe - end - - def parse_non_pre_blocks(text, obj, macros) - s = StringScanner.new(text) - tags = [] - parsed = '' - while !s.eos? - s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) - text, full_tag, closing, tag = s[1], s[2], s[3], s[4] - if tags.empty? - yield text - inject_macros(text, obj, macros) if macros.any? - else - inject_macros(text, obj, macros, false) if macros.any? - end - parsed << text - if tag - if closing - if tags.last == tag.downcase - tags.pop - end - else - tags << tag.downcase - end - parsed << full_tag - end - end - # Close any non closing tags - while tag = tags.pop - parsed << "" - end - parsed - end - - def parse_inline_attachments(text, project, obj, attr, only_path, options) - # when using an image link, try to use an attachment, if possible - attachments = options[:attachments] || [] - attachments += obj.attachments if obj.respond_to?(:attachments) - if attachments.present? - text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| - filename, ext, alt, alttext = $1.downcase, $2, $3, $4 - # search for the picture in attachments - if found = Attachment.latest_attach(attachments, filename) - image_url = url_for :only_path => only_path, :controller => 'attachments', - :action => 'download', :id => found - desc = found.description.to_s.gsub('"', '') - if !desc.blank? && alttext.blank? - alt = " title=\"#{desc}\" alt=\"#{desc}\"" - end - "src=\"#{image_url}\"#{alt}" - else - m - end - end - end - end - - # Wiki links - # - # Examples: - # [[mypage]] - # [[mypage|mytext]] - # wiki links can refer other project wikis, using project name or identifier: - # [[project:]] -> wiki starting page - # [[project:|mytext]] - # [[project:mypage]] - # [[project:mypage|mytext]] - def parse_wiki_links(text, project, obj, attr, only_path, options) - text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| - link_project = project - esc, all, page, title = $1, $2, $3, $5 - if esc.nil? - if page =~ /^([^\:]+)\:(.*)$/ - link_project = Project.find_by_identifier($1) || Project.find_by_name($1) - page = $2 - title ||= $1 if page.blank? - end - - if link_project && link_project.wiki - # extract anchor - anchor = nil - if page =~ /^(.+?)\#(.+)$/ - page, anchor = $1, $2 - end - anchor = sanitize_anchor_name(anchor) if anchor.present? - # check if page exists - wiki_page = link_project.wiki.find_page(page) - url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page - "##{anchor}" - else - case options[:wiki_links] - when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') - when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export - else - wiki_page_id = page.present? ? Wiki.titleize(page) : nil - parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil - url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, - :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) - end - end - link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) - else - # project or wiki doesn't exist - all - end - else - all - end - end - end - - # Redmine links - # - # Examples: - # Issues: - # #52 -> Link to issue #52 - # Changesets: - # r52 -> Link to revision 52 - # commit:a85130f -> Link to scmid starting with a85130f - # Documents: - # document#17 -> Link to document with id 17 - # document:Greetings -> Link to the document with title "Greetings" - # document:"Some document" -> Link to the document with title "Some document" - # Versions: - # version#3 -> Link to version with id 3 - # version:1.0.0 -> Link to version named "1.0.0" - # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" - # Attachments: - # attachment:file.zip -> Link to the attachment of the current object named file.zip - # Source files: - # source:some/file -> Link to the file located at /some/file in the project's repository - # source:some/file@52 -> Link to the file's revision 52 - # source:some/file#L120 -> Link to line 120 of the file - # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 - # export:some/file -> Force the download of the file - # Forum messages: - # message#1218 -> Link to message with id 1218 - # - # Links can refer other objects from other projects, using project identifier: - # identifier:r52 - # identifier:document:"Some document" - # identifier:version:1.0.0 - # identifier:source:some/file - def parse_redmine_links(text, default_project, obj, attr, only_path, options) - text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| - leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 - link = nil - project = default_project - if project_identifier - project = Project.visible.find_by_identifier(project_identifier) - end - if esc.nil? - if prefix.nil? && sep == 'r' - if project - repository = nil - if repo_identifier - repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} - else - repository = project.repository - end - # project.changesets.visible raises an SQL error because of a double join on repositories - if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) - link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision}, - :class => 'changeset', - :title => truncate_single_line(changeset.comments, :length => 100)) - end - end - elsif sep == '#' - oid = identifier.to_i - case prefix - when nil - if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) - anchor = comment_id ? "note-#{comment_id}" : nil - link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, - :class => issue.css_classes, - :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") - end - when 'document' - if document = Document.visible.find_by_id(oid) - link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, - :class => 'document' - end - when 'version' - if version = Version.visible.find_by_id(oid) - link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, - :class => 'version' - end - when 'message' - if message = Message.visible.find_by_id(oid, :include => :parent) - link = link_to_message(message, {:only_path => only_path}, :class => 'message') - end - when 'forum' - if board = Board.visible.find_by_id(oid) - link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, - :class => 'board' - end - when 'news' - if news = News.visible.find_by_id(oid) - link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, - :class => 'news' - end - when 'project' - if p = Project.visible.find_by_id(oid) - link = link_to_project(p, {:only_path => only_path}, :class => 'project') - end - end - elsif sep == ':' - # removes the double quotes if any - name = identifier.gsub(%r{^"(.*)"$}, "\\1") - case prefix - when 'document' - if project && document = project.documents.visible.find_by_title(name) - link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, - :class => 'document' - end - when 'version' - if project && version = project.versions.visible.find_by_name(name) - link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, - :class => 'version' - end - when 'forum' - if project && board = project.boards.visible.find_by_name(name) - link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, - :class => 'board' - end - when 'news' - if project && news = project.news.visible.find_by_title(name) - link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, - :class => 'news' - end - when 'commit', 'source', 'export' - if project - repository = nil - if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$} - repo_prefix, repo_identifier, name = $1, $2, $3 - repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} - else - repository = project.repository - end - if prefix == 'commit' - if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"])) - link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, - :class => 'changeset', - :title => truncate_single_line(h(changeset.comments), :length => 100) - end - else - if repository && User.current.allowed_to?(:browse_repository, project) - name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} - path, rev, anchor = $1, $3, $5 - link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, - :path => to_path_param(path), - :rev => rev, - :anchor => anchor}, - :class => (prefix == 'export' ? 'source download' : 'source') - end - end - repo_prefix = nil - end - when 'attachment' - attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) - if attachments && attachment = Attachment.latest_attach(attachments, name) - link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, - :class => 'attachment' - end - when 'project' - if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) - link = link_to_project(p, {:only_path => only_path}, :class => 'project') - end - end - end - end - (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) - end - end - - HEADING_RE = /(]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) - - def parse_sections(text, project, obj, attr, only_path, options) - return unless options[:edit_section_links] - text.gsub!(HEADING_RE) do - heading = $1 - @current_section += 1 - if @current_section > 1 - content_tag('div', - link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), - :class => 'contextual', - :title => l(:button_edit_section)) + heading.html_safe - else - heading - end - end - end - - # Headings and TOC - # Adds ids and links to headings unless options[:headings] is set to false - def parse_headings(text, project, obj, attr, only_path, options) - return if options[:headings] == false - - text.gsub!(HEADING_RE) do - level, attrs, content = $2.to_i, $3, $4 - item = strip_tags(content).strip - anchor = sanitize_anchor_name(item) - # used for single-file wiki export - anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) - @heading_anchors[anchor] ||= 0 - idx = (@heading_anchors[anchor] += 1) - if idx > 1 - anchor = "#{anchor}-#{idx}" - end - @parsed_headings << [level, anchor, item] - "\n#{content}" - end - end - - MACROS_RE = /( - (!)? # escaping - ( - \{\{ # opening tag - ([\w]+) # macro name - (\(([^\n\r]*?)\))? # optional arguments - ([\n\r].*?[\n\r])? # optional block of text - \}\} # closing tag - ) - )/mx unless const_defined?(:MACROS_RE) - - MACRO_SUB_RE = /( - \{\{ - macro\((\d+)\) - \}\} - )/x unless const_defined?(:MACRO_SUB_RE) - - # Extracts macros from text - def catch_macros(text) - macros = {} - text.gsub!(MACROS_RE) do - all, macro = $1, $4.downcase - if macro_exists?(macro) || all =~ MACRO_SUB_RE - index = macros.size - macros[index] = all - "{{macro(#{index})}}" - else - all - end - end - macros - end - - # Executes and replaces macros in text - def inject_macros(text, obj, macros, execute=true) - text.gsub!(MACRO_SUB_RE) do - all, index = $1, $2.to_i - orig = macros.delete(index) - if execute && orig && orig =~ MACROS_RE - esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) - if esc.nil? - h(exec_macro(macro, obj, args, block) || all) - else - h(all) - end - elsif orig - h(orig) - else - h(all) - end - end - end - - TOC_RE = /

    \{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) - - # Renders the TOC with given headings - def replace_toc(text, headings) - text.gsub!(TOC_RE) do - # Keep only the 4 first levels - headings = headings.select{|level, anchor, item| level <= 4} - if headings.empty? - '' - else - div_class = 'toc' - div_class << ' right' if $1 == '>' - div_class << ' left' if $1 == '<' - out = "

    • " - root = headings.map(&:first).min - current = root - started = false - headings.each do |level, anchor, item| - if level > current - out << '
      • ' * (level - current) - elsif level < current - out << "
      \n" * (current - level) + "
    • " - elsif started - out << '
    • ' - end - out << "#{item}" - current = level - started = true - end - out << '
    ' * (current - root) - out << '' - end - end - end - - # Same as Rails' simple_format helper without using paragraphs - def simple_format_without_paragraph(text) - text.to_s. - gsub(/\r\n?/, "\n"). # \r\n and \r -> \n - gsub(/\n\n+/, "

    "). # 2+ newline -> 2 br - gsub(/([^\n]\n)(?=[^\n])/, '\1
    '). # 1 newline -> br - html_safe - end - - def lang_options_for_select(blank=true) - (blank ? [["(auto)", ""]] : []) + languages_options - end - - def label_tag_for(name, option_tags = nil, options = {}) - label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") - content_tag("label", label_text) - end - - def labelled_form_for(*args, &proc) - args << {} unless args.last.is_a?(Hash) - options = args.last - if args.first.is_a?(Symbol) - options.merge!(:as => args.shift) - end - options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) - form_for(*args, &proc) - end - - def labelled_fields_for(*args, &proc) - args << {} unless args.last.is_a?(Hash) - options = args.last - options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) - fields_for(*args, &proc) - end - - def labelled_remote_form_for(*args, &proc) - ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." - args << {} unless args.last.is_a?(Hash) - options = args.last - options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) - form_for(*args, &proc) - end - - def error_messages_for(*objects) - html = "" - objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact - errors = objects.map {|o| o.errors.full_messages}.flatten - if errors.any? - html << "
      \n" - errors.each do |error| - html << "
    • #{h error}
    • \n" - end - html << "
    \n" - end - html.html_safe - end - - def delete_link(url, options={}) - options = { - :method => :delete, - :data => {:confirm => l(:text_are_you_sure)}, - :class => 'icon icon-del' - }.merge(options) - - link_to l(:button_delete), url, options - end - - def preview_link(url, form, target='preview', options={}) - content_tag 'a', l(:label_preview), { - :href => "#", - :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, - :accesskey => accesskey(:preview) - }.merge(options) - end - - def link_to_function(name, function, html_options={}) - content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) - end - - # Helper to render JSON in views - def raw_json(arg) - arg.to_json.to_s.gsub('/', '\/').html_safe - end - - def back_url - url = params[:back_url] - if url.nil? && referer = request.env['HTTP_REFERER'] - url = CGI.unescape(referer.to_s) - end - url - end - - def back_url_hidden_field_tag - url = back_url - hidden_field_tag('back_url', url, :id => nil) unless url.blank? - end - - def check_all_links(form_name) - link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + - " | ".html_safe + - link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") - end - - def progress_bar(pcts, options={}) - pcts = [pcts, pcts] unless pcts.is_a?(Array) - pcts = pcts.collect(&:round) - pcts[1] = pcts[1] - pcts[0] - pcts << (100 - pcts[1] - pcts[0]) - width = options[:width] || '100px;' - legend = options[:legend] || '' - content_tag('table', - content_tag('tr', - (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + - (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + - (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) - ), :class => 'progress', :style => "width: #{width};").html_safe + - content_tag('p', legend, :class => 'pourcent').html_safe - end - - def checked_image(checked=true) - if checked - image_tag 'toggle_check.png' - end - end - - def context_menu(url) - unless @context_menu_included - content_for :header_tags do - javascript_include_tag('context_menu') + - stylesheet_link_tag('context_menu') - end - if l(:direction) == 'rtl' - content_for :header_tags do - stylesheet_link_tag('context_menu_rtl') - end - end - @context_menu_included = true - end - javascript_tag "contextMenuInit('#{ url_for(url) }')" - end - - def calendar_for(field_id) - include_calendar_headers_tags - javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") - end - - def include_calendar_headers_tags - unless @calendar_headers_tags_included - @calendar_headers_tags_included = true - content_for :header_tags do - start_of_week = Setting.start_of_week - start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? - # Redmine uses 1..7 (monday..sunday) in settings and locales - # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 - start_of_week = start_of_week.to_i % 7 - - tags = javascript_tag( - "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + - "showOn: 'button', buttonImageOnly: true, buttonImage: '" + - path_to_image('/images/calendar.png') + - "', showButtonPanel: true};") - jquery_locale = l('jquery.locale', :default => current_language.to_s) - unless jquery_locale == 'en' - tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") - end - tags - end - end - end - - # Overrides Rails' stylesheet_link_tag with themes and plugins support. - # Examples: - # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults - # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets - # - def stylesheet_link_tag(*sources) - options = sources.last.is_a?(Hash) ? sources.pop : {} - plugin = options.delete(:plugin) - sources = sources.map do |source| - if plugin - "/plugin_assets/#{plugin}/stylesheets/#{source}" - elsif current_theme && current_theme.stylesheets.include?(source) - current_theme.stylesheet_path(source) - else - source - end - end - super sources, options - end - - # Overrides Rails' image_tag with themes and plugins support. - # Examples: - # image_tag('image.png') # => picks image.png from the current theme or defaults - # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets - # - def image_tag(source, options={}) - if plugin = options.delete(:plugin) - source = "/plugin_assets/#{plugin}/images/#{source}" - elsif current_theme && current_theme.images.include?(source) - source = current_theme.image_path(source) - end - super source, options - end - - # Overrides Rails' javascript_include_tag with plugins support - # Examples: - # javascript_include_tag('scripts') # => picks scripts.js from defaults - # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets - # - def javascript_include_tag(*sources) - options = sources.last.is_a?(Hash) ? sources.pop : {} - if plugin = options.delete(:plugin) - sources = sources.map do |source| - if plugin - "/plugin_assets/#{plugin}/javascripts/#{source}" - else - source - end - end - end - super sources, options - end - - def content_for(name, content = nil, &block) - @has_content ||= {} - @has_content[name] = true - super(name, content, &block) - end - - def has_content?(name) - (@has_content && @has_content[name]) || false - end - - def sidebar_content? - has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? - end - - def view_layouts_base_sidebar_hook_response - @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) - end - - def email_delivery_enabled? - !!ActionMailer::Base.perform_deliveries - end - - # Returns the avatar image tag for the given +user+ if avatars are enabled - # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') - def avatar(user, options = { }) - if Setting.gravatar_enabled? - options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) - email = nil - if user.respond_to?(:mail) - email = user.mail - elsif user.to_s =~ %r{<(.+?)>} - email = $1 - end - return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil - else - '' - end - end - - def sanitize_anchor_name(anchor) - if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' - anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') - else - # TODO: remove when ruby1.8 is no longer supported - anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') - end - end - - # Returns the javascript tags that are included in the html layout head - def javascript_heads - tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') - unless User.current.pref.warn_on_leaving_unsaved == '0' - tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") - end - tags - end - - def favicon - "".html_safe - end - - def robot_exclusion_tag - ''.html_safe - end - - # Returns true if arg is expected in the API response - def include_in_api_response?(arg) - unless @included_in_api_response - param = params[:include] - @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') - @included_in_api_response.collect!(&:strip) - end - @included_in_api_response.include?(arg.to_s) - end - - # Returns options or nil if nometa param or X-Redmine-Nometa header - # was set in the request - def api_meta(options) - if params[:nometa].present? || request.headers['X-Redmine-Nometa'] - # compatibility mode for activeresource clients that raise - # an error when unserializing an array with attributes - nil - else - options - end - end - - private - - def wiki_helper - helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) - extend helper - return self - end - - def link_to_content_update(text, url_params = {}, html_options = {}) - link_to(text, url_params, html_options) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/04/04dfe98ce0a611eee30923688999ac146c40ca35.svn-base --- a/.svn/pristine/04/04dfe98ce0a611eee30923688999ac146c40ca35.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Principal < ActiveRecord::Base - self.table_name = "#{table_name_prefix}users#{table_name_suffix}" - - has_many :members, :foreign_key => 'user_id', :dependent => :destroy - has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name" - has_many :projects, :through => :memberships - has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify - - # Groups and active users - scope :active, :conditions => "#{Principal.table_name}.status = 1" - - scope :like, lambda {|q| - q = q.to_s - if q.blank? - where({}) - else - pattern = "%#{q}%" - sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ") - params = {:p => pattern} - if q =~ /^(.+)\s+(.+)$/ - a, b = "#{$1}%", "#{$2}%" - sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))" - sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))" - params.merge!(:a => a, :b => b) - end - where(sql, params) - end - } - - # Principals that are members of a collection of projects - scope :member_of, lambda {|projects| - projects = [projects] unless projects.is_a?(Array) - if projects.empty? - where("1=0") - else - ids = projects.map(&:id) - where("#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids) - end - } - # Principals that are not members of projects - scope :not_member_of, lambda {|projects| - projects = [projects] unless projects.is_a?(Array) - if projects.empty? - where("1=0") - else - ids = projects.map(&:id) - where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids) - end - } - - before_create :set_default_empty_values - - def name(formatter = nil) - to_s - end - - def <=>(principal) - if principal.nil? - -1 - elsif self.class.name == principal.class.name - self.to_s.downcase <=> principal.to_s.downcase - else - # groups after users - principal.class.name <=> self.class.name - end - end - - protected - - # Make sure we don't try to insert NULL values (see #4632) - def set_default_empty_values - self.login ||= '' - self.hashed_password ||= '' - self.firstname ||= '' - self.lastname ||= '' - self.mail ||= '' - true - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/0513c24a062b523037219d9978aab2b119808571.svn-base --- a/.svn/pristine/05/0513c24a062b523037219d9978aab2b119808571.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -<%= form_for @project, - :url => { :action => 'modules', :id => @project }, - :html => {:id => 'modules-form', - :method => :post} do |f| %> - -
    -
    -<%= l(:text_select_project_modules) %> - -<% Redmine::AccessControl.available_project_modules.each do |m| %> -

    -<% end %> -
    -
    - -

    <%= check_all_links 'modules-form' %>

    -

    <%= submit_tag l(:button_save) %>

    - -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/0539624edaa056ab07935479230d09abf0794955.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/0539624edaa056ab07935479230d09abf0794955.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,28 @@ +<% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %> + +
    +
      + <% tabs.each do |tab| -%> +
    • <%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}', this.href); this.blur(); return false;" %>
    • + <% end -%> +
    + +
    + + + +<% tabs.each do |tab| -%> + <%= content_tag('div', render(:partial => tab[:partial], :locals => {:tab => tab} ), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/0548f38a902595a77747491d864f451fe12f0f47.svn-base --- a/.svn/pristine/05/0548f38a902595a77747491d864f451fe12f0f47.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,346 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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. - -RedmineApp::Application.routes.draw do - root :to => 'welcome#index', :as => 'home' - - match 'login', :to => 'account#login', :as => 'signin' - match 'logout', :to => 'account#logout', :as => 'signout' - match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register' - match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password' - match 'account/activate', :to => 'account#activate', :via => :get - - match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news' - match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue' - match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue' - match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue' - - match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post - match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post] - - match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post] - get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message' - match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post] - get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' - - post 'boards/:board_id/topics/preview', :to => 'messages#preview' - post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply' - post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' - post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy' - - # Misc issue routes. TODO: move into resources - match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues' - match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu' - match '/issues/changes', :to => 'journals#index', :as => 'issue_changes' - match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue' - - match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get - match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post] - - match '/projects/:project_id/issues/gantt', :to => 'gantts#show' - match '/issues/gantt', :to => 'gantts#show' - - match '/projects/:project_id/issues/calendar', :to => 'calendars#show' - match '/issues/calendar', :to => 'calendars#show' - - match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get - match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get - - match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post] - match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post] - match 'my/page', :controller => 'my', :action => 'page', :via => :get - match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page - match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post - match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post - match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post] - match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get - match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post - match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post - match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post - - resources :users - match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership' - match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete - match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships' - - match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get - match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post - match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post - match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post - match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post - match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post - match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get - - match 'projects/:id/settings/:tab', :to => "projects#settings" - - resources :projects do - member do - get 'settings' - post 'modules' - post 'archive' - post 'unarchive' - post 'close' - post 'reopen' - match 'copy', :via => [:get, :post] - end - - resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do - collection do - get 'autocomplete' - end - end - - resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy] - - match 'issues/:copy_from/copy', :to => 'issues#new' - resources :issues, :only => [:index, :new, :create] do - resources :time_entries, :controller => 'timelog' do - collection do - get 'report' - end - end - end - # issue form update - match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form' - - resources :files, :only => [:index, :new, :create] - - resources :versions, :except => [:index, :show, :edit, :update, :destroy] do - collection do - put 'close_completed' - end - end - match 'versions.:format', :to => 'versions#index' - match 'roadmap', :to => 'versions#index', :format => false - match 'versions', :to => 'versions#index' - - resources :news, :except => [:show, :edit, :update, :destroy] - resources :time_entries, :controller => 'timelog' do - get 'report', :on => :collection - end - resources :queries, :only => [:new, :create] - resources :issue_categories, :shallow => true - resources :documents, :except => [:show, :edit, :update, :destroy] - resources :boards - resources :repositories, :shallow => true, :except => [:index, :show] do - member do - match 'committers', :via => [:get, :post] - end - end - - match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get - resources :wiki, :except => [:index, :new, :create] do - member do - get 'rename' - post 'rename' - get 'history' - get 'diff' - match 'preview', :via => [:post, :put] - post 'protect' - post 'add_attachment' - end - collection do - get 'export' - get 'date_index' - end - end - match 'wiki', :controller => 'wiki', :action => 'show', :via => :get - get 'wiki/:id/:version', :to => 'wiki#show' - delete 'wiki/:id/:version', :to => 'wiki#destroy_version' - get 'wiki/:id/:version/annotate', :to => 'wiki#annotate' - get 'wiki/:id/:version/diff', :to => 'wiki#diff' - end - - resources :issues do - collection do - match 'bulk_edit', :via => [:get, :post] - post 'bulk_update' - end - resources :time_entries, :controller => 'timelog' do - collection do - get 'report' - end - end - resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy] - end - match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete - - resources :queries, :except => [:show] - - resources :news, :only => [:index, :show, :edit, :update, :destroy] - match '/news/:id/comments', :to => 'comments#create', :via => :post - match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete - - resources :versions, :only => [:show, :edit, :update, :destroy] do - post 'status_by', :on => :member - end - - resources :documents, :only => [:show, :edit, :update, :destroy] do - post 'add_attachment', :on => :member - end - - match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu - - resources :time_entries, :controller => 'timelog', :except => :destroy do - collection do - get 'report' - get 'bulk_edit' - post 'bulk_update' - end - end - match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/ - # TODO: delete /time_entries for bulk deletion - match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete - - # TODO: port to be part of the resources route(s) - match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get - - get 'projects/:id/activity', :to => 'activities#index' - get 'projects/:id/activity.:format', :to => 'activities#index' - get 'activity', :to => 'activities#index' - - # repositories routes - get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats' - get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph' - - get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))', - :to => 'repositories#changes' - - get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision' - get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision' - post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue' - delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue' - get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions' - get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))', - :controller => 'repositories', - :format => false, - :constraints => { - :action => /(browse|show|entry|raw|annotate|diff)/, - :rev => /[a-z0-9\.\-_]+/ - } - - get 'projects/:id/repository/statistics', :to => 'repositories#stats' - get 'projects/:id/repository/graph', :to => 'repositories#graph' - - get 'projects/:id/repository/changes(/*path(.:ext))', - :to => 'repositories#changes' - - get 'projects/:id/repository/revisions', :to => 'repositories#revisions' - get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision' - get 'projects/:id/repository/revision', :to => 'repositories#revision' - post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue' - delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue' - get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))', - :controller => 'repositories', - :format => false, - :constraints => { - :action => /(browse|show|entry|raw|annotate|diff)/, - :rev => /[a-z0-9\.\-_]+/ - } - get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))', - :controller => 'repositories', - :action => /(browse|show|entry|raw|changes|annotate|diff)/ - get 'projects/:id/repository/:action(/*path(.:ext))', - :controller => 'repositories', - :action => /(browse|show|entry|raw|changes|annotate|diff)/ - - get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil - get 'projects/:id/repository', :to => 'repositories#show', :path => nil - - # additional routes for having the file name at the end of url - match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get - match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get - match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get - match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/ - resources :attachments, :only => [:show, :destroy] - - resources :groups do - member do - get 'autocomplete_for_user' - end - end - - match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users' - match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user' - match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post - match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post - - resources :trackers, :except => :show do - collection do - match 'fields', :via => [:get, :post] - end - end - resources :issue_statuses, :except => :show do - collection do - post 'update_issue_done_ratio' - end - end - resources :custom_fields, :except => :show - resources :roles do - collection do - match 'permissions', :via => [:get, :post] - end - end - resources :enumerations, :except => :show - match 'enumerations/:type', :to => 'enumerations#index', :via => :get - - get 'projects/:id/search', :controller => 'search', :action => 'index' - get 'search', :controller => 'search', :action => 'index' - - match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post - - match 'admin', :controller => 'admin', :action => 'index', :via => :get - match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get - match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get - match 'admin/info', :controller => 'admin', :action => 'info', :via => :get - match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get - match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post - - resources :auth_sources do - member do - get 'test_connection' - end - end - - match 'workflows', :controller => 'workflows', :action => 'index', :via => :get - match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post] - match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post] - match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post] - match 'settings', :controller => 'settings', :action => 'index', :via => :get - match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post] - match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post] - - match 'sys/projects', :to => 'sys#projects', :via => :get - match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post - match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get - - match 'uploads', :to => 'attachments#upload', :via => :post - - get 'robots.txt', :to => 'welcome#robots' - - Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir| - file = File.join(plugin_dir, "config/routes.rb") - if File.exists?(file) - begin - instance_eval File.read(file) - rescue Exception => e - puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}." - exit 1 - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/05764e8ea551fb41de1ac455c66cb6f2907259b8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05764e8ea551fb41de1ac455c66cb6f2907259b8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,22 @@ +/* Portuguese initialisation for the jQuery UI date picker plugin. */ +jQuery(function($){ + $.datepicker.regional['pt'] = { + closeText: 'Fechar', + prevText: '<Anterior', + nextText: 'Seguinte', + currentText: 'Hoje', + monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho', + 'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], + monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun', + 'Jul','Ago','Set','Out','Nov','Dez'], + dayNames: ['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'], + dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + weekHeader: 'Sem', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['pt']); +}); diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/058893711420b566bb0870dfb2af89cb84e5e16b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/058893711420b566bb0870dfb2af89cb84e5e16b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,20 @@ +<%= form_tag({:action => 'edit', :tab => 'projects'}) do %> + +
    +

    <%= setting_check_box :default_projects_public %>

    + +

    <%= setting_multiselect(:default_projects_modules, + Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %>

    + +

    <%= setting_multiselect(:default_projects_tracker_ids, + Tracker.sorted.collect {|t| [t.name, t.id.to_s]}) %>

    + +

    <%= setting_check_box :sequential_project_identifiers %>

    + +

    <%= setting_select :new_project_user_role_id, + Role.find_all_givable.collect {|r| [r.name, r.id.to_s]}, + :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %>

    +
    + +<%= submit_tag l(:button_save) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/05bd9a477032dec0ee79cadb6f6626cf79ccae69.svn-base --- a/.svn/pristine/05/05bd9a477032dec0ee79cadb6f6626cf79ccae69.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,431 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingRepositoriesTest < ActionController::IntegrationTest - def setup - @path_hash = repository_path_hash(%w[path to file.c]) - assert_equal "path/to/file.c", @path_hash[:path] - assert_equal "path/to/file.c", @path_hash[:param] - end - - def test_repositories_resources - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repositories/new" }, - { :controller => 'repositories', :action => 'new', :project_id => 'redmine' } - ) - assert_routing( - { :method => 'post', - :path => "/projects/redmine/repositories" }, - { :controller => 'repositories', :action => 'create', :project_id => 'redmine' } - ) - assert_routing( - { :method => 'get', - :path => "/repositories/1/edit" }, - { :controller => 'repositories', :action => 'edit', :id => '1' } - ) - assert_routing( - { :method => 'put', - :path => "/repositories/1" }, - { :controller => 'repositories', :action => 'update', :id => '1' } - ) - assert_routing( - { :method => 'delete', - :path => "/repositories/1" }, - { :controller => 'repositories', :action => 'destroy', :id => '1' } - ) - ["get", "post"].each do |method| - assert_routing( - { :method => method, - :path => "/repositories/1/committers" }, - { :controller => 'repositories', :action => 'committers', :id => '1' } - ) - end - end - - def test_repositories_show - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository" }, - { :controller => 'repositories', :action => 'show', :id => 'redmine' } - ) - end - - def test_repositories - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/statistics" }, - { :controller => 'repositories', :action => 'stats', :id => 'redmine' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/graph" }, - { :controller => 'repositories', :action => 'graph', :id => 'redmine' } - ) - end - - def test_repositories_show_with_repository_id - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo" }, - { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo' } - ) - end - - def test_repositories_with_repository_id - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/statistics" }, - { :controller => 'repositories', :action => 'stats', :id => 'redmine', :repository_id => 'foo' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/graph" }, - { :controller => 'repositories', :action => 'graph', :id => 'redmine', :repository_id => 'foo' } - ) - end - - def test_repositories_revisions - empty_path_param = [] - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions" }, - { :controller => 'repositories', :action => 'revisions', :id => 'redmine' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions.atom" }, - { :controller => 'repositories', :action => 'revisions', :id => 'redmine', - :format => 'atom' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2457" }, - { :controller => 'repositories', :action => 'revision', :id => 'redmine', - :rev => '2457' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2457/show" }, - { :controller => 'repositories', :action => 'show', :id => 'redmine', - :rev => '2457' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2457/show/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'show', :id => 'redmine', - :path => @path_hash[:param] , :rev => '2457'} - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2457/diff" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', - :rev => '2457' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2457/diff" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', - :rev => '2457', :format => 'diff' }, - {}, - { :format => 'diff' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', - :path => @path_hash[:param], :rev => '2' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', - :path => @path_hash[:param], :rev => '2', :format => 'diff' }, - {}, - { :format => 'diff' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2/entry/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'entry', :id => 'redmine', - :path => @path_hash[:param], :rev => '2' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2/raw/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'raw', :id => 'redmine', - :path => @path_hash[:param], :rev => '2' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revisions/2/annotate/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'annotate', :id => 'redmine', - :path => @path_hash[:param], :rev => '2' } - ) - end - - def test_repositories_revisions_with_repository_id - empty_path_param = [] - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions" }, - { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions.atom" }, - { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo', - :format => 'atom' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2457" }, - { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo', - :rev => '2457' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2457/show" }, - { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo', - :rev => '2457' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2457/show/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] , :rev => '2457'} - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2457/diff" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', - :rev => '2457' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2457/diff" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', - :rev => '2457', :format => 'diff' }, - {}, - { :format => 'diff' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param], :rev => '2' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param], :rev => '2', :format => 'diff' }, - {}, - { :format => 'diff' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2/entry/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param], :rev => '2' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2/raw/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'raw', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param], :rev => '2' } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revisions/2/annotate/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param], :rev => '2' } - ) - end - - def test_repositories_non_revisions_path - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/changes" }, - { :controller => 'repositories', :action => 'changes', :id => 'redmine' } - ) - ['2457', 'master', 'slash/slash'].each do |rev| - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/changes" }, - { :controller => 'repositories', :action => 'changes', :id => 'redmine', - :rev => rev }, - {}, - { :rev => rev } - ) - end - ['2457', 'master', 'slash/slash'].each do |rev| - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'changes', :id => 'redmine', - :path => @path_hash[:param], :rev => rev }, - {}, - { :rev => rev } - ) - end - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/diff/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/browse/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'browse', :id => 'redmine', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/entry/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'entry', :id => 'redmine', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/raw/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'raw', :id => 'redmine', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/annotate/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'annotate', :id => 'redmine', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'changes', :id => 'redmine', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/revision" }, - { :controller => 'repositories', :action => 'revision', :id => 'redmine' } - ) - end - - def test_repositories_non_revisions_path_with_repository_id - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/changes" }, - { :controller => 'repositories', :action => 'changes', - :id => 'redmine', :repository_id => 'foo' } - ) - ['2457', 'master', 'slash/slash'].each do |rev| - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/changes" }, - { :controller => 'repositories', :action => 'changes', - :id => 'redmine', - :repository_id => 'foo', :rev => rev }, - {}, - { :rev => rev } - ) - end - ['2457', 'master', 'slash/slash'].each do |rev| - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'changes', :id => 'redmine', - :repository_id => 'foo', :path => @path_hash[:param], :rev => rev }, - {}, - { :rev => rev } - ) - end - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/diff/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/browse/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'browse', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/entry/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/raw/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'raw', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/annotate/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" }, - { :controller => 'repositories', :action => 'changes', :id => 'redmine', :repository_id => 'foo', - :path => @path_hash[:param] } - ) - assert_routing( - { :method => 'get', - :path => "/projects/redmine/repository/foo/revision" }, - { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo'} - ) - end - - def test_repositories_related_issues - assert_routing( - { :method => 'post', - :path => "/projects/redmine/repository/revisions/123/issues" }, - { :controller => 'repositories', :action => 'add_related_issue', - :id => 'redmine', :rev => '123' } - ) - assert_routing( - { :method => 'delete', - :path => "/projects/redmine/repository/revisions/123/issues/25" }, - { :controller => 'repositories', :action => 'remove_related_issue', - :id => 'redmine', :rev => '123', :issue_id => '25' } - ) - end - - def test_repositories_related_issues_with_repository_id - assert_routing( - { :method => 'post', - :path => "/projects/redmine/repository/foo/revisions/123/issues" }, - { :controller => 'repositories', :action => 'add_related_issue', - :id => 'redmine', :repository_id => 'foo', :rev => '123' } - ) - assert_routing( - { :method => 'delete', - :path => "/projects/redmine/repository/foo/revisions/123/issues/25" }, - { :controller => 'repositories', :action => 'remove_related_issue', - :id => 'redmine', :repository_id => 'foo', :rev => '123', :issue_id => '25' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/05/05c2095e86e69b0ded063e085ccf41b6310d9ac0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05c2095e86e69b0ded063e085ccf41b6310d9ac0.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,3 @@ +<%= title l(:label_custom_field_plural) %> + +<%= render_tabs custom_fields_tabs %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/06/06191abb72622bec9f1387de935e8ce335158e32.svn-base --- a/.svn/pristine/06/06191abb72622bec9f1387de935e8ce335158e32.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,255 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2012 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 AttachmentTest < ActiveSupport::TestCase - fixtures :users, :projects, :roles, :members, :member_roles, - :enabled_modules, :issues, :trackers, :attachments - - class MockFile - attr_reader :original_filename, :content_type, :content, :size - - def initialize(attributes) - @original_filename = attributes[:original_filename] - @content_type = attributes[:content_type] - @content = attributes[:content] || "Content" - @size = content.size - end - end - - def setup - set_tmp_attachments_directory - end - - def test_container_for_new_attachment_should_be_nil - assert_nil Attachment.new.container - end - - def test_create - a = Attachment.new(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", "text/plain"), - :author => User.find(1)) - assert a.save - assert_equal 'testfile.txt', a.filename - assert_equal 59, a.filesize - assert_equal 'text/plain', a.content_type - assert_equal 0, a.downloads - assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest - assert File.exist?(a.diskfile) - assert_equal 59, File.size(a.diskfile) - end - - def test_size_should_be_validated_for_new_file - with_settings :attachment_max_size => 0 do - a = Attachment.new(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", "text/plain"), - :author => User.find(1)) - assert !a.save - end - end - - def test_size_should_not_be_validated_when_copying - a = Attachment.create!(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", "text/plain"), - :author => User.find(1)) - with_settings :attachment_max_size => 0 do - copy = a.copy - assert copy.save - end - end - - def test_description_length_should_be_validated - a = Attachment.new(:description => 'a' * 300) - assert !a.save - assert_not_nil a.errors[:description] - end - - def test_destroy - a = Attachment.new(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", "text/plain"), - :author => User.find(1)) - assert a.save - assert_equal 'testfile.txt', a.filename - assert_equal 59, a.filesize - assert_equal 'text/plain', a.content_type - assert_equal 0, a.downloads - assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest - diskfile = a.diskfile - assert File.exist?(diskfile) - assert_equal 59, File.size(a.diskfile) - assert a.destroy - assert !File.exist?(diskfile) - end - - def test_destroy_should_not_delete_file_referenced_by_other_attachment - a = Attachment.create!(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", "text/plain"), - :author => User.find(1)) - diskfile = a.diskfile - - copy = a.copy - copy.save! - - assert File.exists?(diskfile) - a.destroy - assert File.exists?(diskfile) - copy.destroy - assert !File.exists?(diskfile) - end - - def test_create_should_auto_assign_content_type - a = Attachment.new(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", ""), - :author => User.find(1)) - assert a.save - assert_equal 'text/plain', a.content_type - end - - def test_identical_attachments_at_the_same_time_should_not_overwrite - a1 = Attachment.create!(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", ""), - :author => User.find(1)) - a2 = Attachment.create!(:container => Issue.find(1), - :file => uploaded_test_file("testfile.txt", ""), - :author => User.find(1)) - assert a1.disk_filename != a2.disk_filename - end - - def test_filename_should_be_basenamed - a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file")) - assert_equal 'file', a.filename - end - - def test_filename_should_be_sanitized - a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars")) - assert_equal 'valid_[] invalid_chars', a.filename - end - - def test_diskfilename - assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/ - assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1] - assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1] - assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1] - assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1] - end - - def test_title - a = Attachment.new(:filename => "test.png") - assert_equal "test.png", a.title - - a = Attachment.new(:filename => "test.png", :description => "Cool image") - assert_equal "test.png (Cool image)", a.title - end - - def test_prune_should_destroy_old_unattached_attachments - Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago) - Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago) - Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1) - - assert_difference 'Attachment.count', -2 do - Attachment.prune - end - end - - context "Attachmnet.attach_files" do - should "attach the file" do - issue = Issue.first - assert_difference 'Attachment.count' do - Attachment.attach_files(issue, - '1' => { - 'file' => uploaded_test_file('testfile.txt', 'text/plain'), - 'description' => 'test' - }) - end - - attachment = Attachment.first(:order => 'id DESC') - assert_equal issue, attachment.container - assert_equal 'testfile.txt', attachment.filename - assert_equal 59, attachment.filesize - assert_equal 'test', attachment.description - assert_equal 'text/plain', attachment.content_type - assert File.exists?(attachment.diskfile) - assert_equal 59, File.size(attachment.diskfile) - end - - should "add unsaved files to the object as unsaved attachments" do - # Max size of 0 to force Attachment creation failures - with_settings(:attachment_max_size => 0) do - @project = Project.find(1) - response = Attachment.attach_files(@project, { - '1' => {'file' => mock_file, 'description' => 'test'}, - '2' => {'file' => mock_file, 'description' => 'test'} - }) - - assert response[:unsaved].present? - assert_equal 2, response[:unsaved].length - assert response[:unsaved].first.new_record? - assert response[:unsaved].second.new_record? - assert_equal response[:unsaved], @project.unsaved_attachments - end - end - end - - def test_latest_attach - set_fixtures_attachments_directory - a1 = Attachment.find(16) - assert_equal "testfile.png", a1.filename - assert a1.readable? - assert (! a1.visible?(User.anonymous)) - assert a1.visible?(User.find(2)) - a2 = Attachment.find(17) - assert_equal "testfile.PNG", a2.filename - assert a2.readable? - assert (! a2.visible?(User.anonymous)) - assert a2.visible?(User.find(2)) - assert a1.created_on < a2.created_on - - la1 = Attachment.latest_attach([a1, a2], "testfile.png") - assert_equal 17, la1.id - la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG") - assert_equal 17, la2.id - - set_tmp_attachments_directory - end - - def test_thumbnailable_should_be_true_for_images - assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable? - end - - def test_thumbnailable_should_be_true_for_non_images - assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable? - end - - if convert_installed? - def test_thumbnail_should_generate_the_thumbnail - set_fixtures_attachments_directory - attachment = Attachment.find(16) - Attachment.clear_thumbnails - - assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do - thumbnail = attachment.thumbnail - assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail) - assert File.exists?(thumbnail) - end - end - else - puts '(ImageMagick convert not available)' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/06/06594ea20a7f9dbceb45bfdd779a780237719c5b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06594ea20a7f9dbceb45bfdd779a780237719c5b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,123 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueStatusesControllerTest < ActionController::TestCase + fixtures :issue_statuses, :issues, :users + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_index_by_anonymous_should_redirect_to_login_form + @request.session[:user_id] = nil + get :index + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fissue_statuses' + end + + def test_index_by_user_should_respond_with_406 + @request.session[:user_id] = 2 + get :index + assert_response 406 + end + + def test_new + get :new + assert_response :success + assert_template 'new' + end + + def test_create + assert_difference 'IssueStatus.count' do + post :create, :issue_status => {:name => 'New status'} + end + assert_redirected_to :action => 'index' + status = IssueStatus.order('id DESC').first + assert_equal 'New status', status.name + end + + def test_create_with_failure + post :create, :issue_status => {:name => ''} + assert_response :success + assert_template 'new' + assert_error_tag :content => /name can't be blank/i + end + + def test_edit + get :edit, :id => '3' + assert_response :success + assert_template 'edit' + end + + def test_update + put :update, :id => '3', :issue_status => {:name => 'Renamed status'} + assert_redirected_to :action => 'index' + status = IssueStatus.find(3) + assert_equal 'Renamed status', status.name + end + + def test_update_with_failure + put :update, :id => '3', :issue_status => {:name => ''} + assert_response :success + assert_template 'edit' + assert_error_tag :content => /name can't be blank/i + end + + def test_destroy + Issue.delete_all("status_id = 1") + + assert_difference 'IssueStatus.count', -1 do + delete :destroy, :id => '1' + end + assert_redirected_to :action => 'index' + assert_nil IssueStatus.find_by_id(1) + end + + def test_destroy_should_block_if_status_in_use + assert_not_nil Issue.find_by_status_id(1) + + assert_no_difference 'IssueStatus.count' do + delete :destroy, :id => '1' + end + assert_redirected_to :action => 'index' + assert_not_nil IssueStatus.find_by_id(1) + end + + def test_update_issue_done_ratio_with_issue_done_ratio_set_to_issue_field + with_settings :issue_done_ratio => 'issue_field' do + post :update_issue_done_ratio + assert_match /not updated/, flash[:error].to_s + assert_redirected_to '/issue_statuses' + end + end + + def test_update_issue_done_ratio_with_issue_done_ratio_set_to_issue_status + with_settings :issue_done_ratio => 'issue_status' do + post :update_issue_done_ratio + assert_match /Issue done ratios updated/, flash[:notice].to_s + assert_redirected_to '/issue_statuses' + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/06/067d843bc0cda3da0f5457a92adeb100a2eb241d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/067d843bc0cda3da0f5457a92adeb100a2eb241d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,30 @@ + + + + + + <% if tab[:name] == 'IssueCustomField' %> + + + <% end %> + + + + + <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> + "> + + + + <% if tab[:name] == 'IssueCustomField' %> + + + <% end %> + + + + <% end; reset_cycle %> + +
    <%=l(:field_name)%><%=l(:field_field_format)%><%=l(:field_is_required)%><%=l(:field_is_for_all)%><%=l(:label_used_by)%><%=l(:button_sort)%>
    <%= link_to h(custom_field.name), edit_custom_field_path(custom_field) %><%= l(custom_field.format.label) %><%= checked_image custom_field.is_required? %><%= checked_image custom_field.is_for_all? %><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %><%= reorder_links('custom_field', {:action => 'update', :id => custom_field}, :put) %> + <%= delete_link custom_field_path(custom_field) %> +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/06/06d3a68b8a48539fa27a23262cfda3ac72728bc2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06d3a68b8a48539fa27a23262cfda3ac72728bc2.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1135 @@ +# Danish translation file for standard Ruby on Rails internationalization +# by Lars Hoeg (http://www.lenio.dk/) +# updated and upgraded to 0.9 by Morten Krogh Andersen (http://www.krogh.net) + +da: + direction: ltr + date: + formats: + default: "%d.%m.%Y" + short: "%e. %b %Y" + long: "%e. %B %Y" + + day_names: [søndag, mandag, tirsdag, onsdag, torsdag, fredag, lørdag] + abbr_day_names: [sø, ma, ti, 'on', to, fr, lø] + month_names: [~, januar, februar, marts, april, maj, juni, juli, august, september, oktober, november, december] + abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, aug, sep, okt, nov, dec] + order: + - :day + - :month + - :year + + time: + formats: + default: "%e. %B %Y, %H:%M" + time: "%H:%M" + short: "%e. %b %Y, %H:%M" + long: "%A, %e. %B %Y, %H:%M" + am: "" + pm: "" + + support: + array: + sentence_connector: "og" + skip_last_comma: true + + datetime: + distance_in_words: + half_a_minute: "et halvt minut" + less_than_x_seconds: + one: "mindre end et sekund" + other: "mindre end %{count} sekunder" + x_seconds: + one: "et sekund" + other: "%{count} sekunder" + less_than_x_minutes: + one: "mindre end et minut" + other: "mindre end %{count} minutter" + x_minutes: + one: "et minut" + other: "%{count} minutter" + about_x_hours: + one: "cirka en time" + other: "cirka %{count} timer" + x_hours: + one: "1 time" + other: "%{count} timer" + x_days: + one: "en dag" + other: "%{count} dage" + about_x_months: + one: "cirka en måned" + other: "cirka %{count} måneder" + x_months: + one: "en måned" + other: "%{count} måneder" + about_x_years: + one: "cirka et år" + other: "cirka %{count} år" + over_x_years: + one: "mere end et år" + other: "mere end %{count} år" + almost_x_years: + one: "næsten 1 år" + other: "næsten %{count} år" + + number: + format: + separator: "," + delimiter: "." + precision: 3 + currency: + format: + format: "%u %n" + unit: "DKK" + separator: "," + delimiter: "." + precision: 2 + precision: + format: + # separator: + delimiter: "" + # precision: + human: + format: + # separator: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + percentage: + format: + # separator: + delimiter: "" + # precision: + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "er ikke i listen" + exclusion: "er reserveret" + invalid: "er ikke gyldig" + confirmation: "stemmer ikke overens" + accepted: "skal accepteres" + empty: "må ikke udelades" + blank: "skal udfyldes" + too_long: "er for lang (højst %{count} tegn)" + too_short: "er for kort (mindst %{count} tegn)" + wrong_length: "har forkert længde (skulle være %{count} tegn)" + taken: "er allerede anvendt" + not_a_number: "er ikke et tal" + greater_than: "skal være større end %{count}" + greater_than_or_equal_to: "skal være større end eller lig med %{count}" + equal_to: "skal være lig med %{count}" + less_than: "skal være mindre end %{count}" + less_than_or_equal_to: "skal være mindre end eller lig med %{count}" + odd: "skal være ulige" + even: "skal være lige" + greater_than_start_date: "skal være senere end startdatoen" + not_same_project: "hører ikke til samme projekt" + circular_dependency: "Denne relation vil skabe et afhængighedsforhold" + cant_link_an_issue_with_a_descendant: "En sag kan ikke relateres til en af dens underopgaver" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + template: + header: + one: "En fejl forhindrede %{model} i at blive gemt" + other: "%{count} fejl forhindrede denne %{model} i at blive gemt" + body: "Der var problemer med følgende felter:" + + actionview_instancetag_blank_option: Vælg venligst + + general_text_No: 'Nej' + general_text_Yes: 'Ja' + general_text_no: 'nej' + general_text_yes: 'ja' + general_lang_name: 'Danish (Dansk)' + general_csv_separator: ',' + general_csv_encoding: ISO-8859-1 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Kontoen er opdateret. + notice_account_invalid_creditentials: Ugyldig bruger og/eller kodeord + notice_account_password_updated: Kodeordet er opdateret. + notice_account_wrong_password: Forkert kodeord + notice_account_register_done: Kontoen er oprettet. For at aktivere kontoen skal du klikke på linket i den tilsendte email. + notice_account_unknown_email: Ukendt bruger. + notice_can_t_change_password: Denne konto benytter en ekstern sikkerhedsgodkendelse. Det er ikke muligt at skifte kodeord. + notice_account_lost_email_sent: En email med instruktioner til at vælge et nyt kodeord er afsendt til dig. + notice_account_activated: Din konto er aktiveret. Du kan nu logge ind. + notice_successful_create: Succesfuld oprettelse. + notice_successful_update: Succesfuld opdatering. + notice_successful_delete: Succesfuld sletning. + notice_successful_connection: Succesfuld forbindelse. + notice_file_not_found: Siden du forsøger at tilgå eksisterer ikke eller er blevet fjernet. + notice_locking_conflict: Data er opdateret af en anden bruger. + notice_not_authorized: Du har ikke adgang til denne side. + notice_email_sent: "En email er sendt til %{value}" + notice_email_error: "En fejl opstod under afsendelse af email (%{value})" + notice_feeds_access_key_reseted: Din adgangsnøgle til Atom er nulstillet. + notice_failed_to_save_issues: "Det mislykkedes at gemme %{count} sage(r) på %{total} valgt: %{ids}." + notice_no_issue_selected: "Ingen sag er valgt! Vælg venligst hvilke emner du vil rette." + notice_account_pending: "Din konto er oprettet, og afventer administrators godkendelse." + notice_default_data_loaded: Standardopsætningen er indlæst. + + error_can_t_load_default_data: "Standardopsætning kunne ikke indlæses: %{value}" + error_scm_not_found: "Adgang nægtet og/eller revision blev ikke fundet i det valgte repository." + error_scm_command_failed: "En fejl opstod under forbindelsen til det valgte repository: %{value}" + + mail_subject_lost_password: "Dit %{value} kodeord" + mail_body_lost_password: 'Klik på dette link for at ændre dit kodeord:' + mail_subject_register: "%{value} kontoaktivering" + mail_body_register: 'Klik på dette link for at aktivere din konto:' + mail_body_account_information_external: "Du kan bruge din %{value} konto til at logge ind." + mail_body_account_information: Din kontoinformation + mail_subject_account_activation_request: "%{value} kontoaktivering" + mail_body_account_activation_request: "En ny bruger (%{value}) er registreret. Godkend venligst kontoen:" + + + field_name: Navn + field_description: Beskrivelse + field_summary: Sammenfatning + field_is_required: Skal udfyldes + field_firstname: Fornavn + field_lastname: Efternavn + field_mail: Email + field_filename: Fil + field_filesize: Størrelse + field_downloads: Downloads + field_author: Forfatter + field_created_on: Oprettet + field_updated_on: Opdateret + field_field_format: Format + field_is_for_all: For alle projekter + field_possible_values: Mulige værdier + field_regexp: Regulære udtryk + field_min_length: Mindste længde + field_max_length: Største længde + field_value: Værdi + field_category: Kategori + field_title: Titel + field_project: Projekt + field_issue: Sag + field_status: Status + field_notes: Noter + field_is_closed: Sagen er lukket + field_is_default: Standardværdi + field_tracker: Type + field_subject: Emne + field_due_date: Deadline + field_assigned_to: Tildelt til + field_priority: Prioritet + field_fixed_version: Udgave + field_user: Bruger + field_role: Rolle + field_homepage: Hjemmeside + field_is_public: Offentlig + field_parent: Underprojekt af + field_is_in_roadmap: Sager vist i roadmap + field_login: Login + field_mail_notification: Email-påmindelser + field_admin: Administrator + field_last_login_on: Sidste forbindelse + field_language: Sprog + field_effective_date: Dato + field_password: Kodeord + field_new_password: Nyt kodeord + field_password_confirmation: Bekræft + field_version: Version + field_type: Type + field_host: Vært + field_port: Port + field_account: Kode + field_base_dn: Base DN + field_attr_login: Login attribut + field_attr_firstname: Fornavn attribut + field_attr_lastname: Efternavn attribut + field_attr_mail: Email attribut + field_onthefly: løbende brugeroprettelse + field_start_date: Start dato + field_done_ratio: "% færdig" + field_auth_source: Sikkerhedsmetode + field_hide_mail: Skjul min email + field_comments: Kommentar + field_url: URL + field_start_page: Startside + field_subproject: Underprojekt + field_hours: Timer + field_activity: Aktivitet + field_spent_on: Dato + field_identifier: Identifikator + field_is_filter: Brugt som et filter + field_issue_to: Beslægtede sag + field_delay: Udsættelse + field_assignable: Sager kan tildeles denne rolle + field_redirect_existing_links: Videresend eksisterende links + field_estimated_hours: Anslået tid + field_column_names: Kolonner + field_time_zone: Tidszone + field_searchable: Søgbar + field_default_value: Standardværdi + + setting_app_title: Applikationstitel + setting_app_subtitle: Applikationsundertekst + setting_welcome_text: Velkomsttekst + setting_default_language: Standardsporg + setting_login_required: Sikkerhed påkrævet + setting_self_registration: Brugeroprettelse + setting_attachment_max_size: Vedhæftede filers max størrelse + setting_issues_export_limit: Sagseksporteringsbegrænsning + setting_mail_from: Afsender-email + setting_bcc_recipients: Skjult modtager (bcc) + setting_host_name: Værtsnavn + setting_text_formatting: Tekstformatering + setting_wiki_compression: Komprimering af wiki-historik + setting_feeds_limit: Feed indholdsbegrænsning + setting_autofetch_changesets: Hent automatisk commits + setting_sys_api_enabled: Aktiver webservice for automatisk administration af repository + setting_commit_ref_keywords: Referencenøgleord + setting_commit_fix_keywords: Afslutningsnøgleord + setting_autologin: Automatisk login + setting_date_format: Datoformat + setting_time_format: Tidsformat + setting_cross_project_issue_relations: Tillad sagsrelationer på tværs af projekter + setting_issue_list_default_columns: Standardkolonner på sagslisten + setting_emails_footer: Email-fodnote + setting_protocol: Protokol + setting_user_format: Brugervisningsformat + + project_module_issue_tracking: Sagssøgning + project_module_time_tracking: Tidsstyring + project_module_news: Nyheder + project_module_documents: Dokumenter + project_module_files: Filer + project_module_wiki: Wiki + project_module_repository: Repository + project_module_boards: Fora + + label_user: Bruger + label_user_plural: Brugere + label_user_new: Ny bruger + label_project: Projekt + label_project_new: Nyt projekt + label_project_plural: Projekter + label_x_projects: + zero: Ingen projekter + one: 1 projekt + other: "%{count} projekter" + label_project_all: Alle projekter + label_project_latest: Seneste projekter + label_issue: Sag + label_issue_new: Opret sag + label_issue_plural: Sager + label_issue_view_all: Vis alle sager + label_issues_by: "Sager fra %{value}" + label_issue_added: Sagen er oprettet + label_issue_updated: Sagen er opdateret + label_document: Dokument + label_document_new: Nyt dokument + label_document_plural: Dokumenter + label_document_added: Dokument tilføjet + label_role: Rolle + label_role_plural: Roller + label_role_new: Ny rolle + label_role_and_permissions: Roller og rettigheder + label_member: Medlem + label_member_new: Nyt medlem + label_member_plural: Medlemmer + label_tracker: Type + label_tracker_plural: Typer + label_tracker_new: Ny type + label_workflow: Arbejdsgang + label_issue_status: Sagsstatus + label_issue_status_plural: Sagsstatusser + label_issue_status_new: Ny status + label_issue_category: Sagskategori + label_issue_category_plural: Sagskategorier + label_issue_category_new: Ny kategori + label_custom_field: Brugerdefineret felt + label_custom_field_plural: Brugerdefinerede felter + label_custom_field_new: Nyt brugerdefineret felt + label_enumerations: Værdier + label_enumeration_new: Ny værdi + label_information: Information + label_information_plural: Information + label_please_login: Login + label_register: Registrér + label_password_lost: Glemt kodeord + label_home: Forside + label_my_page: Min side + label_my_account: Min konto + label_my_projects: Mine projekter + label_administration: Administration + label_login: Log ind + label_logout: Log ud + label_help: Hjælp + label_reported_issues: Rapporterede sager + label_assigned_to_me_issues: Sager tildelt til mig + label_last_login: Sidste logintidspunkt + label_registered_on: Registreret den + label_activity: Aktivitet + label_new: Ny + label_logged_as: Registreret som + label_environment: Miljø + label_authentication: Sikkerhed + label_auth_source: Sikkerhedsmetode + label_auth_source_new: Ny sikkerhedsmetode + label_auth_source_plural: Sikkerhedsmetoder + label_subproject_plural: Underprojekter + label_min_max_length: Min - Max længde + label_list: Liste + label_date: Dato + label_integer: Heltal + label_float: Kommatal + label_boolean: Sand/falsk + label_string: Tekst + label_text: Lang tekst + label_attribute: Attribut + label_attribute_plural: Attributter + label_no_data: Ingen data at vise + label_change_status: Ændringsstatus + label_history: Historik + label_attachment: Fil + label_attachment_new: Ny fil + label_attachment_delete: Slet fil + label_attachment_plural: Filer + label_file_added: Fil tilføjet + label_report: Rapport + label_report_plural: Rapporter + label_news: Nyheder + label_news_new: Tilføj nyheder + label_news_plural: Nyheder + label_news_latest: Seneste nyheder + label_news_view_all: Vis alle nyheder + label_news_added: Nyhed tilføjet + label_settings: Indstillinger + label_overview: Oversigt + label_version: Udgave + label_version_new: Ny udgave + label_version_plural: Udgaver + label_confirmation: Bekræftelser + label_export_to: Eksporter til + label_read: Læs... + label_public_projects: Offentlige projekter + label_open_issues: åben + label_open_issues_plural: åbne + label_closed_issues: lukket + label_closed_issues_plural: lukkede + label_x_open_issues_abbr_on_total: + zero: 0 åbne / %{total} + one: 1 åben / %{total} + other: "%{count} åbne / %{total}" + label_x_open_issues_abbr: + zero: 0 åbne + one: 1 åben + other: "%{count} åbne" + label_x_closed_issues_abbr: + zero: 0 lukkede + one: 1 lukket + other: "%{count} lukkede" + label_total: Total + label_permissions: Rettigheder + label_current_status: Nuværende status + label_new_statuses_allowed: Ny status tilladt + label_all: alle + label_none: intet + label_nobody: ingen + label_next: Næste + label_previous: Forrige + label_used_by: Brugt af + label_details: Detaljer + label_add_note: Tilføj note + label_per_page: Pr. side + label_calendar: Kalender + label_months_from: måneder frem + label_gantt: Gantt + label_internal: Intern + label_last_changes: "sidste %{count} ændringer" + label_change_view_all: Vis alle ændringer + label_personalize_page: Tilret denne side + label_comment: Kommentar + label_comment_plural: Kommentarer + label_x_comments: + zero: ingen kommentarer + one: 1 kommentar + other: "%{count} kommentarer" + label_comment_add: Tilføj en kommentar + label_comment_added: Kommentaren er tilføjet + label_comment_delete: Slet kommentar + label_query: Brugerdefineret forespørgsel + label_query_plural: Brugerdefinerede forespørgsler + label_query_new: Ny forespørgsel + label_filter_add: Tilføj filter + label_filter_plural: Filtre + label_equals: er + label_not_equals: er ikke + label_in_less_than: er mindre end + label_in_more_than: er større end + label_in: indeholdt i + label_today: i dag + label_all_time: altid + label_yesterday: i går + label_this_week: denne uge + label_last_week: sidste uge + label_last_n_days: "sidste %{count} dage" + label_this_month: denne måned + label_last_month: sidste måned + label_this_year: dette år + label_date_range: Dato interval + label_less_than_ago: mindre end dage siden + label_more_than_ago: mere end dage siden + label_ago: dage siden + label_contains: indeholder + label_not_contains: ikke indeholder + label_day_plural: dage + label_repository: Repository + label_repository_plural: Repositories + label_browse: Gennemse + label_revision: Revision + label_revision_plural: Revisioner + label_associated_revisions: Tilknyttede revisioner + label_added: tilføjet + label_modified: ændret + label_deleted: slettet + label_latest_revision: Seneste revision + label_latest_revision_plural: Seneste revisioner + label_view_revisions: Se revisioner + label_max_size: Maksimal størrelse + label_sort_highest: Flyt til toppen + label_sort_higher: Flyt op + label_sort_lower: Flyt ned + label_sort_lowest: Flyt til bunden + label_roadmap: Roadmap + label_roadmap_due_in: Deadline + label_roadmap_overdue: "%{value} forsinket" + label_roadmap_no_issues: Ingen sager i denne version + label_search: Søg + label_result_plural: Resultater + label_all_words: Alle ord + label_wiki: Wiki + label_wiki_edit: Wiki ændring + label_wiki_edit_plural: Wiki ændringer + label_wiki_page: Wiki side + label_wiki_page_plural: Wiki sider + label_index_by_title: Indhold efter titel + label_index_by_date: Indhold efter dato + label_current_version: Nuværende version + label_preview: Forhåndsvisning + label_feed_plural: Feeds + label_changes_details: Detaljer for alle ændringer + label_issue_tracking: Sagssøgning + label_spent_time: Anvendt tid + label_f_hour: "%{value} time" + label_f_hour_plural: "%{value} timer" + label_time_tracking: Tidsstyring + label_change_plural: Ændringer + label_statistics: Statistik + label_commits_per_month: Commits pr. måned + label_commits_per_author: Commits pr. bruger + label_view_diff: Vis forskelle + label_diff_inline: inline + label_diff_side_by_side: side om side + label_options: Formatering + label_copy_workflow_from: Kopier arbejdsgang fra + label_permissions_report: Godkendelsesrapport + label_watched_issues: Overvågede sager + label_related_issues: Relaterede sager + label_applied_status: Anvendte statusser + label_loading: Indlæser... + label_relation_new: Ny relation + label_relation_delete: Slet relation + label_relates_to: relaterer til + label_duplicates: duplikater + label_blocks: blokerer + label_blocked_by: blokeret af + label_precedes: kommer før + label_follows: følger + label_end_to_start: slut til start + label_end_to_end: slut til slut + label_start_to_start: start til start + label_start_to_end: start til slut + label_stay_logged_in: Forbliv indlogget + label_disabled: deaktiveret + label_show_completed_versions: Vis færdige versioner + label_me: mig + label_board: Forum + label_board_new: Nyt forum + label_board_plural: Fora + label_topic_plural: Emner + label_message_plural: Beskeder + label_message_last: Sidste besked + label_message_new: Ny besked + label_message_posted: Besked tilføjet + label_reply_plural: Besvarer + label_send_information: Send konto information til bruger + label_year: År + label_month: Måned + label_week: Uge + label_date_from: Fra + label_date_to: Til + label_language_based: Baseret på brugerens sprog + label_sort_by: "Sortér efter %{value}" + label_send_test_email: Send en test email + label_feeds_access_key_created_on: "Atom adgangsnøgle dannet for %{value} siden" + label_module_plural: Moduler + label_added_time_by: "Tilføjet af %{author} for %{age} siden" + label_updated_time: "Opdateret for %{value} siden" + label_jump_to_a_project: Skift til projekt... + label_file_plural: Filer + label_changeset_plural: Ændringer + label_default_columns: Standardkolonner + label_no_change_option: (Ingen ændringer) + label_bulk_edit_selected_issues: Masse-ret de valgte sager + label_theme: Tema + label_default: standard + label_search_titles_only: Søg kun i titler + label_user_mail_option_all: "For alle hændelser på mine projekter" + label_user_mail_option_selected: "For alle hændelser på de valgte projekter..." + label_user_mail_no_self_notified: "Jeg ønsker ikke besked om ændring foretaget af mig selv" + label_registration_activation_by_email: kontoaktivering på email + label_registration_manual_activation: manuel kontoaktivering + label_registration_automatic_activation: automatisk kontoaktivering + label_display_per_page: "Per side: %{value}" + label_age: Alder + label_change_properties: Ændre indstillinger + label_general: Generelt + label_more: Mere + label_scm: SCM + label_plugins: Plugins + label_ldap_authentication: LDAP-godkendelse + label_downloads_abbr: D/L + + button_login: Login + button_submit: Send + button_save: Gem + button_check_all: Vælg alt + button_uncheck_all: Fravælg alt + button_delete: Slet + button_create: Opret + button_test: Test + button_edit: Ret + button_add: Tilføj + button_change: Ændre + button_apply: Anvend + button_clear: Nulstil + button_lock: Lås + button_unlock: Lås op + button_download: Download + button_list: List + button_view: Vis + button_move: Flyt + button_back: Tilbage + button_cancel: Annullér + button_activate: Aktivér + button_sort: Sortér + button_log_time: Log tid + button_rollback: Tilbagefør til denne version + button_watch: Overvåg + button_unwatch: Stop overvågning + button_reply: Besvar + button_archive: Arkivér + button_unarchive: Fjern fra arkiv + button_reset: Nulstil + button_rename: Omdøb + button_change_password: Skift kodeord + button_copy: Kopiér + button_annotate: Annotér + button_update: Opdatér + button_configure: Konfigurér + + status_active: aktiv + status_registered: registreret + status_locked: låst + + text_select_mail_notifications: Vælg handlinger der skal sendes email besked for. + text_regexp_info: f.eks. ^[A-ZÆØÅ0-9]+$ + text_min_max_length_info: 0 betyder ingen begrænsninger + text_project_destroy_confirmation: Er du sikker på at du vil slette dette projekt og alle relaterede data? + text_workflow_edit: Vælg en rolle samt en type, for at redigere arbejdsgangen + text_are_you_sure: Er du sikker? + text_tip_issue_begin_day: opgaven begynder denne dag + text_tip_issue_end_day: opaven slutter denne dag + text_tip_issue_begin_end_day: opgaven begynder og slutter denne dag + text_caracters_maximum: "max %{count} karakterer." + text_caracters_minimum: "Skal være mindst %{count} karakterer lang." + text_length_between: "Længde skal være mellem %{min} og %{max} karakterer." + text_tracker_no_workflow: Ingen arbejdsgang defineret for denne type + text_unallowed_characters: Ikke-tilladte karakterer + text_comma_separated: Adskillige værdier tilladt (adskilt med komma). + text_issues_ref_in_commit_messages: Referer og løser sager i commit-beskeder + text_issue_added: "Sag %{id} er rapporteret af %{author}." + text_issue_updated: "Sag %{id} er blevet opdateret af %{author}." + text_wiki_destroy_confirmation: Er du sikker på at du vil slette denne wiki og dens indhold? + text_issue_category_destroy_question: "Nogle sager (%{count}) er tildelt denne kategori. Hvad ønsker du at gøre?" + text_issue_category_destroy_assignments: Slet tildelinger af kategori + text_issue_category_reassign_to: Tildel sager til denne kategori + text_user_mail_option: "For ikke-valgte projekter vil du kun modtage beskeder omhandlende ting du er involveret i eller overvåger (f.eks. sager du har indberettet eller ejer)." + text_no_configuration_data: "Roller, typer, sagsstatusser og arbejdsgange er endnu ikke konfigureret.\nDet er anbefalet at indlæse standardopsætningen. Du vil kunne ændre denne når den er indlæst." + text_load_default_configuration: Indlæs standardopsætningen + text_status_changed_by_changeset: "Anvendt i ændring %{value}." + text_issues_destroy_confirmation: 'Er du sikker på du ønsker at slette den/de valgte sag(er)?' + text_select_project_modules: 'Vælg moduler er skal være aktiveret for dette projekt:' + text_default_administrator_account_changed: Standardadministratorkonto ændret + text_file_repository_writable: Filarkiv er skrivbar + text_rmagick_available: RMagick tilgængelig (valgfri) + + default_role_manager: Leder + default_role_developer: Udvikler + default_role_reporter: Rapportør + default_tracker_bug: Fejl + default_tracker_feature: Funktion + default_tracker_support: Support + default_issue_status_new: Ny + default_issue_status_in_progress: Igangværende + default_issue_status_resolved: Løst + default_issue_status_feedback: Feedback + default_issue_status_closed: Lukket + default_issue_status_rejected: Afvist + default_doc_category_user: Brugerdokumentation + default_doc_category_tech: Teknisk dokumentation + default_priority_low: Lav + default_priority_normal: Normal + default_priority_high: Høj + default_priority_urgent: Akut + default_priority_immediate: Omgående + default_activity_design: Design + default_activity_development: Udvikling + + enumeration_issue_priorities: Sagsprioriteter + enumeration_doc_categories: Dokumentkategorier + enumeration_activities: Aktiviteter (tidsstyring) + + label_add_another_file: Tilføj endnu en fil + label_chronological_order: I kronologisk rækkefølge + setting_activity_days_default: Antal dage der vises under projektaktivitet + text_destroy_time_entries_question: "%{hours} timer er registreret på denne sag som du er ved at slette. Hvad vil du gøre?" + error_issue_not_found_in_project: 'Sagen blev ikke fundet eller tilhører ikke dette projekt' + text_assign_time_entries_to_project: Tildel raporterede timer til projektet + setting_display_subprojects_issues: Vis sager for underprojekter på hovedprojektet som standard + label_optional_description: Valgfri beskrivelse + text_destroy_time_entries: Slet registrerede timer + field_comments_sorting: Vis kommentar + text_reassign_time_entries: 'Tildel registrerede timer til denne sag igen' + label_reverse_chronological_order: I omvendt kronologisk rækkefølge + label_preferences: Præferencer + label_overall_activity: Overordnet aktivitet + setting_default_projects_public: Nye projekter er offentlige som standard + error_scm_annotate: "Filen findes ikke, eller kunne ikke annoteres." + label_planning: Planlægning + text_subprojects_destroy_warning: "Dets underprojekter(er): %{value} vil også blive slettet." + permission_edit_issues: Redigér sager + setting_diff_max_lines_displayed: Højeste antal forskelle der vises + permission_edit_own_issue_notes: Redigér egne noter + setting_enabled_scm: Aktiveret SCM + button_quote: Citér + permission_view_files: Se filer + permission_add_issues: Tilføj sager + permission_edit_own_messages: Redigér egne beskeder + permission_delete_own_messages: Slet egne beskeder + permission_manage_public_queries: Administrér offentlig forespørgsler + permission_log_time: Registrér anvendt tid + label_renamed: omdøbt + label_incoming_emails: Indkommende emails + permission_view_changesets: Se ændringer + permission_manage_versions: Administrér versioner + permission_view_time_entries: Se anvendt tid + label_generate_key: Generér en nøglefil + permission_manage_categories: Administrér sagskategorier + permission_manage_wiki: Administrér wiki + setting_sequential_project_identifiers: Generér sekventielle projekt-identifikatorer + setting_plain_text_mail: Emails som almindelig tekst (ingen HTML) + field_parent_title: Siden over + text_email_delivery_not_configured: "Email-afsendelse er ikke indstillet og notifikationer er defor slået fra.\nKonfigurér din SMTP server i config/configuration.yml og genstart applikationen for at aktivere email-afsendelse." + permission_protect_wiki_pages: Beskyt wiki sider + permission_add_issue_watchers: Tilføj overvågere + warning_attachments_not_saved: "der var %{count} fil(er), som ikke kunne gemmes." + permission_comment_news: Kommentér nyheder + text_enumeration_category_reassign_to: 'Flyt dem til denne værdi:' + permission_select_project_modules: Vælg projektmoduler + permission_view_gantt: Se Gantt diagram + permission_delete_messages: Slet beskeder + permission_move_issues: Flyt sager + permission_edit_wiki_pages: Redigér wiki sider + label_user_activity: "%{value}'s aktivitet" + permission_manage_issue_relations: Administrér sags-relationer + label_issue_watchers: Overvågere + permission_delete_wiki_pages: Slet wiki sider + notice_unable_delete_version: Kan ikke slette versionen. + permission_view_wiki_edits: Se wiki historik + field_editable: Redigérbar + label_duplicated_by: dubleret af + permission_manage_boards: Administrér fora + permission_delete_wiki_pages_attachments: Slet filer vedhæftet wiki sider + permission_view_messages: Se beskeder + text_enumeration_destroy_question: "%{count} objekter er tildelt denne værdi." + permission_manage_files: Administrér filer + permission_add_messages: Opret beskeder + permission_edit_issue_notes: Redigér noter + permission_manage_news: Administrér nyheder + text_plugin_assets_writable: Der er skriverettigheder til plugin assets folderen + label_display: Vis + label_and_its_subprojects: "%{value} og dets underprojekter" + permission_view_calendar: Se kalender + button_create_and_continue: Opret og fortsæt + setting_gravatar_enabled: Anvend Gravatar brugerikoner + label_updated_time_by: "Opdateret af %{author} for %{age} siden" + text_diff_truncated: '... Listen over forskelle er blevet afkortet da den overstiger den maksimale størrelse der kan vises.' + text_user_wrote: "%{value} skrev:" + setting_mail_handler_api_enabled: Aktiver webservice for indkomne emails + permission_delete_issues: Slet sager + permission_view_documents: Se dokumenter + permission_browse_repository: Gennemse repository + permission_manage_repository: Administrér repository + permission_manage_members: Administrér medlemmer + mail_subject_reminder: "%{count} sag(er) har deadline i de kommende dage (%{days})" + permission_add_issue_notes: Tilføj noter + permission_edit_messages: Redigér beskeder + permission_view_issue_watchers: Se liste over overvågere + permission_commit_access: Commit adgang + setting_mail_handler_api_key: API nøgle + label_example: Eksempel + permission_rename_wiki_pages: Omdøb wiki sider + text_custom_field_possible_values_info: 'En linje for hver værdi' + permission_view_wiki_pages: Se wiki + permission_edit_project: Redigér projekt + permission_save_queries: Gem forespørgsler + label_copied: kopieret + text_repository_usernames_mapping: "Vælg eller opdatér de Redmine brugere der svarer til de enkelte brugere fundet i repository loggen.\nBrugere med samme brugernavn eller email adresse i både Redmine og det valgte repository bliver automatisk koblet sammen." + permission_edit_time_entries: Redigér tidsregistreringer + general_csv_decimal_separator: ',' + permission_edit_own_time_entries: Redigér egne tidsregistreringer + setting_repository_log_display_limit: Højeste antal revisioner vist i fil-log + setting_file_max_size_displayed: Maksimale størrelse på tekstfiler vist inline + field_watcher: Overvåger + setting_openid: Tillad OpenID login og registrering + field_identity_url: OpenID URL + label_login_with_open_id_option: eller login med OpenID + setting_per_page_options: Enheder per side muligheder + mail_body_reminder: "%{count} sage(er) som er tildelt dig har deadline indenfor de næste %{days} dage:" + field_content: Indhold + label_descending: Aftagende + label_sort: Sortér + label_ascending: Tiltagende + label_date_from_to: Fra %{start} til %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Denne side har %{descendants} underside(r) og afledte. Hvad vil du gøre? + text_wiki_page_reassign_children: Flyt undersider til denne side + text_wiki_page_nullify_children: Behold undersider som rod-sider + text_wiki_page_destroy_children: Slet undersider ogalle deres afledte sider. + setting_password_min_length: Mindste længde på kodeord + field_group_by: Gruppér resultater efter + mail_subject_wiki_content_updated: "'%{id}' wikisiden er blevet opdateret" + label_wiki_content_added: Wiki side tilføjet + mail_subject_wiki_content_added: "'%{id}' wikisiden er blevet tilføjet" + mail_body_wiki_content_added: The '%{id}' wikiside er blevet tilføjet af %{author}. + label_wiki_content_updated: Wikiside opdateret + mail_body_wiki_content_updated: Wikisiden '%{id}' er blevet opdateret af %{author}. + permission_add_project: Opret projekt + setting_new_project_user_role_id: Denne rolle gives til en bruger, som ikke er administrator, og som opretter et projekt + label_view_all_revisions: Se alle revisioner + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: Der er ingen sagshåndtering for dette projekt. Kontrollér venligst projektindstillingerne. + error_no_default_issue_status: Der er ikke defineret en standardstatus. Kontrollér venligst indstillingerne (gå til "Administration -> Sagsstatusser"). + text_journal_changed: "%{label} ændret fra %{old} til %{new}" + text_journal_set_to: "%{label} sat til %{value}" + text_journal_deleted: "%{label} slettet (%{old})" + label_group_plural: Grupper + label_group: Grupper + label_group_new: Ny gruppe + label_time_entry_plural: Anvendt tid + text_journal_added: "%{label} %{value} tilføjet" + field_active: Aktiv + enumeration_system_activity: System Aktivitet + permission_delete_issue_watchers: Slet overvågere + version_status_closed: lukket + version_status_locked: låst + version_status_open: åben + error_can_not_reopen_issue_on_closed_version: En sag tildelt en lukket version kan ikke genåbnes + label_user_anonymous: Anonym + button_move_and_follow: Flyt og overvåg + setting_default_projects_modules: Standard moduler, aktiveret for nye projekter + setting_gravatar_default: Standard Gravatar billede + field_sharing: Delning + label_version_sharing_hierarchy: Med projekthierarki + label_version_sharing_system: Med alle projekter + label_version_sharing_descendants: Med underprojekter + label_version_sharing_tree: Med projekttræ + label_version_sharing_none: Ikke delt + error_can_not_archive_project: Dette projekt kan ikke arkiveres + button_duplicate: Duplikér + button_copy_and_follow: Kopiér og overvåg + label_copy_source: Kilde + setting_issue_done_ratio: Beregn sagsløsning ratio + setting_issue_done_ratio_issue_status: Benyt sagsstatus + error_issue_done_ratios_not_updated: Sagsløsnings ratio, ikke opdateret. + error_workflow_copy_target: Vælg venligst måltracker og rolle(r) + setting_issue_done_ratio_issue_field: Benyt sagsfelt + label_copy_same_as_target: Samme som mål + label_copy_target: Mål + notice_issue_done_ratios_updated: Sagsløsningsratio opdateret. + error_workflow_copy_source: Vælg venligst en kildetracker eller rolle + label_update_issue_done_ratios: Opdater sagsløsningsratio + setting_start_of_week: Start kalendre på + permission_view_issues: Vis sager + label_display_used_statuses_only: Vis kun statusser der er benyttet af denne tracker + label_revision_id: Revision %{value} + label_api_access_key: API nøgle + label_api_access_key_created_on: API nøgle genereret %{value} siden + label_feeds_access_key: Atom nøgle + notice_api_access_key_reseted: Din API nøgle er nulstillet. + setting_rest_api_enabled: Aktiver REST web service + label_missing_api_access_key: Mangler en API nøgle + label_missing_feeds_access_key: Mangler en Atom nøgle + button_show: Vis + text_line_separated: Flere væredier tilladt (en linje for hver værdi). + setting_mail_handler_body_delimiters: Trunkér emails efter en af disse linjer + permission_add_subprojects: Lav underprojekter + label_subproject_new: Nyt underprojekt + text_own_membership_delete_confirmation: |- + Du er ved at fjerne en eller flere af dine rettigheder, og kan muligvis ikke redigere projektet bagefter. + Er du sikker på du ønsker at fortsætte? + label_close_versions: Luk færdige versioner + label_board_sticky: Klistret + label_board_locked: Låst + permission_export_wiki_pages: Eksporter wiki sider + setting_cache_formatted_text: Cache formatteret tekst + permission_manage_project_activities: Administrer projektaktiviteter + error_unable_delete_issue_status: Det var ikke muligt at slette sagsstatus + label_profile: Profil + permission_manage_subtasks: Administrer underopgaver + field_parent_issue: Hovedopgave + label_subtask_plural: Underopgaver + label_project_copy_notifications: Send email notifikationer, mens projektet kopieres + error_can_not_delete_custom_field: Kan ikke slette brugerdefineret felt + error_unable_to_connect: Kan ikke forbinde (%{value}) + error_can_not_remove_role: Denne rolle er i brug og kan ikke slettes. + error_can_not_delete_tracker: Denne type indeholder sager og kan ikke slettes. + field_principal: Principal + label_my_page_block: blok + notice_failed_to_save_members: "Fejl under lagring af medlem(mer): %{errors}." + text_zoom_out: Zoom ud + text_zoom_in: Zoom ind + notice_unable_delete_time_entry: Kan ikke slette tidsregistrering. + label_overall_spent_time: Overordnet forbrug af tid + field_time_entries: Log tid + project_module_gantt: Gantt + project_module_calendar: Kalender + button_edit_associated_wikipage: "Redigér tilknyttet Wiki side: %{page_title}" + field_text: Tekstfelt + label_user_mail_option_only_owner: Kun for ting jeg er ejer af + setting_default_notification_option: Standardpåmindelsesmulighed + label_user_mail_option_only_my_events: Kun for ting jeg overvåger eller er involveret i + label_user_mail_option_only_assigned: Kun for ting jeg er tildelt + label_user_mail_option_none: Ingen hændelser + field_member_of_group: Medlem af gruppe + field_assigned_to_role: Medlem af rolle + notice_not_authorized_archived_project: Projektet du prøver at tilgå, er blevet arkiveret. + label_principal_search: "Søg efter bruger eller gruppe:" + label_user_search: "Søg efter bruger:" + field_visible: Synlig + setting_commit_logtime_activity_id: Aktivitet for registreret tid + text_time_logged_by_changeset: Anvendt i changeset %{value}. + setting_commit_logtime_enabled: Aktiver tidsregistrering + notice_gantt_chart_truncated: Kortet er blevet afkortet, fordi det overstiger det maksimale antal elementer, der kan vises (%{max}) + setting_gantt_items_limit: Maksimalt antal af elementer der kan vises på gantt kortet + + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: Kodning af Commit beskeder + 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 sag + one: 1 sag + other: "%{count} sager" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: alle + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Med underprojekter + label_cross_project_tree: Med projekttræ + label_cross_project_hierarchy: Med projekthierarki + label_cross_project_system: Med alle projekter + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Total + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/06/06dd3ee8641385912fee02a84e6de53eed535f28.svn-base --- a/.svn/pristine/06/06dd3ee8641385912fee02a84e6de53eed535f28.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -
    -<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %> -
    - -

    <%=l(:label_auth_source_plural)%>

    - - - - - - - - - - -<% for source in @auth_sources %> - "> - - - - - - -<% end %> - -
    <%=l(:field_name)%><%=l(:field_type)%><%=l(:field_host)%><%=l(:label_user_plural)%>
    <%= link_to(h(source.name), :action => 'edit', :id => source)%><%= h source.auth_method_name %><%= h source.host %><%= h source.users.count %> - <%= link_to l(:button_test), {:action => 'test_connection', :id => source}, :class => 'icon icon-test' %> - <%= delete_link auth_source_path(source) %> -
    - -

    <%= pagination_links_full @auth_source_pages %>

    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/071604c5dacdea2b6b0884f22cd68afddd0f1a2a.svn-base --- a/.svn/pristine/07/071604c5dacdea2b6b0884f22cd68afddd0f1a2a.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CvsAdapterTest < ActiveSupport::TestCase - REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s - REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? - MODULE_NAME = 'test' - - if File.directory?(REPOSITORY_PATH) - def setup - @adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH) - end - - def test_scm_version - to_test = { "\nConcurrent Versions System (CVS) 1.12.13 (client/server)\n" => [1,12,13], - "\r\n1.12.12\r\n1.12.11" => [1,12,12], - "1.12.11\r\n1.12.10\r\n" => [1,12,11]} - to_test.each do |s, v| - test_scm_version_for(s, v) - end - end - - def test_revisions_all - cnt = 0 - @adapter.revisions('', nil, nil, :log_encoding => 'UTF-8') do |revision| - cnt += 1 - end - assert_equal 16, cnt - end - - def test_revisions_from_rev3 - rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22) - cnt = 0 - @adapter.revisions('', rev3_committed_on, nil, :log_encoding => 'UTF-8') do |revision| - cnt += 1 - end - assert_equal 4, cnt - end - - def test_entries_rev3 - rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22) - entries = @adapter.entries('sources', rev3_committed_on) - assert_equal 2, entries.size - assert_equal entries[0].name, "watchers_controller.rb" - assert_equal entries[0].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22) - end - - def test_path_encoding_default_utf8 - adpt1 = Redmine::Scm::Adapters::CvsAdapter.new( - MODULE_NAME, - REPOSITORY_PATH - ) - assert_equal "UTF-8", adpt1.path_encoding - adpt2 = Redmine::Scm::Adapters::CvsAdapter.new( - MODULE_NAME, - REPOSITORY_PATH, - nil, - nil, - "" - ) - assert_equal "UTF-8", adpt2.path_encoding - end - - private - - def test_scm_version_for(scm_command_version, version) - @adapter.class.expects(:scm_version_from_command_line).returns(scm_command_version) - assert_equal version, @adapter.class.scm_command_version - end - else - puts "Cvs test repository NOT FOUND. Skipping unit tests !!!" - def test_fake; assert true end - end - end - -rescue LoadError - class CvsMochaFake < ActiveSupport::TestCase - def test_fake; assert(false, "Requires mocha to run those tests") end - end -end - diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/072d65dcf460d00cc4770641f038bc5801ca9243.svn-base --- a/.svn/pristine/07/072d65dcf460d00cc4770641f038bc5801ca9243.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2012 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 PrincipalTest < ActiveSupport::TestCase - fixtures :users, :projects, :members, :member_roles - - def test_active_scope_should_return_groups_and_active_users - result = Principal.active.all - assert_include Group.first, result - assert_not_nil result.detect {|p| p.is_a?(User)} - assert_nil result.detect {|p| p.is_a?(User) && !p.active?} - assert_nil result.detect {|p| p.is_a?(AnonymousUser)} - end - - def test_member_of_scope_should_return_the_union_of_all_members - projects = Project.find_all_by_id(1, 2) - assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort - end - - def test_member_of_scope_should_be_empty_for_no_projects - assert_equal [], Principal.member_of([]).sort - end - - def test_not_member_of_scope_should_return_users_that_have_no_memberships - projects = Project.find_all_by_id(1, 2) - expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort - assert_equal expected, Principal.not_member_of(projects).sort - end - - def test_not_member_of_scope_should_be_empty_for_no_projects - assert_equal [], Principal.not_member_of([]).sort - end - - context "#like" do - setup do - Principal.create!(:login => 'login') - Principal.create!(:login => 'login2') - - Principal.create!(:firstname => 'firstname') - Principal.create!(:firstname => 'firstname2') - - Principal.create!(:lastname => 'lastname') - Principal.create!(:lastname => 'lastname2') - - Principal.create!(:mail => 'mail@example.com') - Principal.create!(:mail => 'mail2@example.com') - - @palmer = Principal.create!(:firstname => 'David', :lastname => 'Palmer') - end - - should "search login" do - results = Principal.like('login') - - assert_equal 2, results.count - assert results.all? {|u| u.login.match(/login/) } - end - - should "search firstname" do - results = Principal.like('firstname') - - assert_equal 2, results.count - assert results.all? {|u| u.firstname.match(/firstname/) } - end - - should "search lastname" do - results = Principal.like('lastname') - - assert_equal 2, results.count - assert results.all? {|u| u.lastname.match(/lastname/) } - end - - should "search mail" do - results = Principal.like('mail') - - assert_equal 2, results.count - assert results.all? {|u| u.mail.match(/mail/) } - end - - should "search firstname and lastname" do - results = Principal.like('david palm') - - assert_equal 1, results.count - assert_equal @palmer, results.first - end - - should "search lastname and firstname" do - results = Principal.like('palmer davi') - - assert_equal 1, results.count - assert_equal @palmer, results.first - end - end - - def test_like_scope_with_cyrillic_name - user = User.generate!(:firstname => 'Соболев', :lastname => 'ДениÑ') - results = Principal.like('Собо') - assert_equal 1, results.count - assert_equal user, results.first - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/072ff390fa0205dbe10e54ecdcfb356c9a41489e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/072ff390fa0205dbe10e54ecdcfb356c9a41489e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,34 @@ +

    <%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %>

    + +<%= labelled_form_for @user, :url => register_path do |f| %> +<%= error_messages_for 'user' %> + +
    +<% if @user.auth_source_id.nil? %> +

    <%= f.text_field :login, :size => 25, :required => true %>

    + +

    <%= f.password_field :password, :size => 25, :required => true %> + <%= l(:text_caracters_minimum, :count => Setting.password_min_length) %>

    + +

    <%= f.password_field :password_confirmation, :size => 25, :required => true %>

    +<% end %> + +

    <%= f.text_field :firstname, :required => true %>

    +

    <%= f.text_field :lastname, :required => true %>

    +

    <%= f.text_field :mail, :required => true %>

    + +<% unless @user.force_default_language? %> +

    <%= f.select :language, lang_options_for_select %>

    +<% end %> + +<% if Setting.openid? %> +

    <%= f.text_field :identity_url %>

    +<% end %> + +<% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %> +

    <%= custom_field_tag_with_label :user, value %>

    +<% end %> +
    + +<%= submit_tag l(:button_submit) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/0760e17a4ae9d6716b2c149e62df20d426be1e60.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/0760e17a4ae9d6716b2c149e62df20d426be1e60.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,86 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) + +module RedmineMenuTestHelper + # Assertions + def assert_number_of_items_in_menu(menu_name, count) + assert Redmine::MenuManager.items(menu_name).size >= count, "Menu has less than #{count} items" + end + + def assert_menu_contains_item_named(menu_name, item_name) + assert Redmine::MenuManager.items(menu_name).collect(&:name).include?(item_name.to_sym), "Menu did not have an item named #{item_name}" + end + + # Helpers + def get_menu_item(menu_name, item_name) + Redmine::MenuManager.items(menu_name).find {|item| item.name == item_name.to_sym} + end +end + +class RedmineTest < ActiveSupport::TestCase + include RedmineMenuTestHelper + + def test_top_menu + assert_number_of_items_in_menu :top_menu, 5 + assert_menu_contains_item_named :top_menu, :home + assert_menu_contains_item_named :top_menu, :my_page + assert_menu_contains_item_named :top_menu, :projects + assert_menu_contains_item_named :top_menu, :administration + assert_menu_contains_item_named :top_menu, :help + end + + def test_account_menu + assert_number_of_items_in_menu :account_menu, 4 + assert_menu_contains_item_named :account_menu, :login + assert_menu_contains_item_named :account_menu, :register + assert_menu_contains_item_named :account_menu, :my_account + assert_menu_contains_item_named :account_menu, :logout + end + + def test_application_menu + assert_number_of_items_in_menu :application_menu, 0 + end + + def test_admin_menu + assert_number_of_items_in_menu :admin_menu, 0 + end + + def test_project_menu + assert_number_of_items_in_menu :project_menu, 14 + assert_menu_contains_item_named :project_menu, :overview + assert_menu_contains_item_named :project_menu, :activity + assert_menu_contains_item_named :project_menu, :roadmap + assert_menu_contains_item_named :project_menu, :issues + assert_menu_contains_item_named :project_menu, :new_issue + assert_menu_contains_item_named :project_menu, :calendar + assert_menu_contains_item_named :project_menu, :gantt + assert_menu_contains_item_named :project_menu, :news + assert_menu_contains_item_named :project_menu, :documents + assert_menu_contains_item_named :project_menu, :wiki + assert_menu_contains_item_named :project_menu, :boards + assert_menu_contains_item_named :project_menu, :files + assert_menu_contains_item_named :project_menu, :repository + assert_menu_contains_item_named :project_menu, :settings + end + + def test_new_issue_should_have_root_as_a_parent + new_issue = get_menu_item(:project_menu, :new_issue) + assert_equal :root, new_issue.parent.name + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/076dd28036e1c2d5496bf9c0c53bccefc9c747b1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/076dd28036e1c2d5496bf9c0c53bccefc9c747b1.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1137 @@ +# Hungarian translations for Ruby on Rails +# by Richard Abonyi (richard.abonyi@gmail.com) +# thanks to KKata, replaced and #hup.hu +# Cleaned up by László Bácsi (http://lackac.hu) +# updated by kfl62 kfl62g@gmail.com +# updated by Gábor Takács (taky77@gmail.com) + +"hu": + direction: ltr + date: + formats: + default: "%Y.%m.%d." + short: "%b %e." + long: "%Y. %B %e." + day_names: [vasárnap, hétfÅ‘, kedd, szerda, csütörtök, péntek, szombat] + abbr_day_names: [v., h., k., sze., cs., p., szo.] + month_names: [~, január, február, március, április, május, június, július, augusztus, szeptember, október, november, december] + abbr_month_names: [~, jan., febr., márc., ápr., máj., jún., júl., aug., szept., okt., nov., dec.] + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y. %b %d., %H:%M" + time: "%H:%M" + short: "%b %e., %H:%M" + long: "%Y. %B %e., %A, %H:%M" + am: "de." + pm: "du." + + datetime: + distance_in_words: + half_a_minute: 'fél perc' + less_than_x_seconds: +# zero: 'kevesebb, mint 1 másodperce' + one: 'kevesebb, mint 1 másodperce' + other: 'kevesebb, mint %{count} másodperce' + x_seconds: + one: '1 másodperce' + other: '%{count} másodperce' + less_than_x_minutes: +# zero: 'kevesebb, mint 1 perce' + one: 'kevesebb, mint 1 perce' + other: 'kevesebb, mint %{count} perce' + x_minutes: + one: '1 perce' + other: '%{count} perce' + about_x_hours: + one: 'csaknem 1 órája' + other: 'csaknem %{count} órája' + x_hours: + one: "1 óra" + other: "%{count} óra" + x_days: + one: '1 napja' + other: '%{count} napja' + about_x_months: + one: 'csaknem 1 hónapja' + other: 'csaknem %{count} hónapja' + x_months: + one: '1 hónapja' + other: '%{count} hónapja' + about_x_years: + one: 'csaknem 1 éve' + other: 'csaknem %{count} éve' + over_x_years: + one: 'több, mint 1 éve' + other: 'több, mint %{count} éve' + almost_x_years: + one: "csaknem 1 éve" + other: "csaknem %{count} éve" + prompts: + year: "Év" + month: "Hónap" + day: "Nap" + hour: "Óra" + minute: "Perc" + second: "Másodperc" + + number: + format: + precision: 2 + separator: ',' + delimiter: ' ' + currency: + format: + unit: 'Ft' + precision: 0 + format: '%n %u' + separator: "," + delimiter: "" + percentage: + format: + delimiter: "" + precision: + format: + delimiter: "" + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "bájt" + other: "bájt" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: +# sentence_connector: "és" +# skip_last_comma: true + words_connector: ", " + two_words_connector: " és " + last_word_connector: " és " + activerecord: + errors: + template: + header: + one: "1 hiba miatt nem menthetÅ‘ a következÅ‘: %{model}" + other: "%{count} hiba miatt nem menthetÅ‘ a következÅ‘: %{model}" + body: "Problémás mezÅ‘k:" + messages: + inclusion: "nincs a listában" + exclusion: "nem elérhetÅ‘" + invalid: "nem megfelelÅ‘" + confirmation: "nem egyezik" + accepted: "nincs elfogadva" + empty: "nincs megadva" + blank: "nincs megadva" + too_long: "túl hosszú (nem lehet több %{count} karakternél)" + too_short: "túl rövid (legalább %{count} karakter kell legyen)" + wrong_length: "nem megfelelÅ‘ hosszúságú (%{count} karakter szükséges)" + taken: "már foglalt" + not_a_number: "nem szám" + greater_than: "nagyobb kell legyen, mint %{count}" + greater_than_or_equal_to: "legalább %{count} kell legyen" + equal_to: "pontosan %{count} kell legyen" + less_than: "kevesebb, mint %{count} kell legyen" + less_than_or_equal_to: "legfeljebb %{count} lehet" + odd: "páratlan kell legyen" + even: "páros kell legyen" + greater_than_start_date: "nagyobbnak kell lennie, mint az indítás dátuma" + not_same_project: "nem azonos projekthez tartozik" + circular_dependency: "Ez a kapcsolat egy körkörös függÅ‘séget eredményez" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Kérem válasszon + + general_text_No: 'Nem' + general_text_Yes: 'Igen' + general_text_no: 'nem' + general_text_yes: 'igen' + general_lang_name: 'Magyar' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-2 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: A fiók adatai sikeresen frissítve. + notice_account_invalid_creditentials: Hibás felhasználói név, vagy jelszó + notice_account_password_updated: A jelszó módosítása megtörtént. + notice_account_wrong_password: Hibás jelszó + notice_account_register_done: A fiók sikeresen létrehozva. Aktiválásához kattints az e-mailben kapott linkre + notice_account_unknown_email: Ismeretlen felhasználó. + notice_can_t_change_password: A fiók külsÅ‘ azonosítási forrást használ. A jelszó megváltoztatása nem lehetséges. + notice_account_lost_email_sent: Egy e-mail üzenetben postáztunk Önnek egy leírást az új jelszó beállításáról. + notice_account_activated: Fiókját aktiváltuk. Most már be tud jelentkezni a rendszerbe. + notice_successful_create: Sikeres létrehozás. + notice_successful_update: Sikeres módosítás. + notice_successful_delete: Sikeres törlés. + notice_successful_connection: Sikeres bejelentkezés. + notice_file_not_found: Az oldal, amit meg szeretne nézni nem található, vagy átkerült egy másik helyre. + notice_locking_conflict: Az adatot egy másik felhasználó idÅ‘ közben módosította. + notice_not_authorized: Nincs hozzáférési engedélye ehhez az oldalhoz. + notice_email_sent: "Egy e-mail üzenetet küldtünk a következÅ‘ címre %{value}" + notice_email_error: "Hiba történt a levél küldése közben (%{value})" + notice_feeds_access_key_reseted: Az Atom hozzáférési kulcsát újra generáltuk. + notice_failed_to_save_issues: "Nem sikerült a %{count} feladat(ok) mentése a %{total} -ban kiválasztva: %{ids}." + notice_no_issue_selected: "Nincs feladat kiválasztva! Kérem jelölje meg melyik feladatot szeretné szerkeszteni!" + notice_account_pending: "A fiókja létrejött, és adminisztrátori jóváhagyásra vár." + notice_default_data_loaded: Az alapértelmezett konfiguráció betöltése sikeresen megtörtént. + + error_can_t_load_default_data: "Az alapértelmezett konfiguráció betöltése nem lehetséges: %{value}" + error_scm_not_found: "A bejegyzés, vagy revízió nem található a tárolóban." + error_scm_command_failed: "A tároló elérése közben hiba lépett fel: %{value}" + error_scm_annotate: "A bejegyzés nem létezik, vagy nics jegyzetekkel ellátva." + error_issue_not_found_in_project: 'A feladat nem található, vagy nem ehhez a projekthez tartozik' + + mail_subject_lost_password: Az Ön Redmine jelszava + mail_body_lost_password: 'A Redmine jelszó megváltoztatásához, kattintson a következÅ‘ linkre:' + mail_subject_register: Redmine azonosító aktiválása + mail_body_register: 'A Redmine azonosítója aktiválásához, kattintson a következÅ‘ linkre:' + mail_body_account_information_external: "A %{value} azonosító használatával bejelentkezhet a Redmine-ba." + mail_body_account_information: Az Ön Redmine azonosítójának információi + mail_subject_account_activation_request: Redmine azonosító aktiválási kérelem + mail_body_account_activation_request: "Egy új felhasználó (%{value}) regisztrált, azonosítója jóváhasgyásra várakozik:" + + + field_name: Név + field_description: Leírás + field_summary: Összegzés + field_is_required: KötelezÅ‘ + field_firstname: Keresztnév + field_lastname: Vezetéknév + field_mail: E-mail + field_filename: Fájl + field_filesize: Méret + field_downloads: Letöltések + field_author: SzerzÅ‘ + field_created_on: Létrehozva + field_updated_on: Módosítva + field_field_format: Formátum + field_is_for_all: Minden projekthez + field_possible_values: Lehetséges értékek + field_regexp: Reguláris kifejezés + field_min_length: Minimum hossz + field_max_length: Maximum hossz + field_value: Érték + field_category: Kategória + field_title: Cím + field_project: Projekt + field_issue: Feladat + field_status: Státusz + field_notes: Feljegyzések + field_is_closed: Feladat lezárva + field_is_default: Alapértelmezett érték + field_tracker: Típus + field_subject: Tárgy + field_due_date: Befejezés dátuma + field_assigned_to: FelelÅ‘s + field_priority: Prioritás + field_fixed_version: Cél verzió + field_user: Felhasználó + field_role: Szerepkör + field_homepage: Weboldal + field_is_public: Nyilvános + field_parent: SzülÅ‘ projekt + field_is_in_roadmap: Feladatok látszanak az életútban + field_login: Azonosító + field_mail_notification: E-mail értesítések + field_admin: Adminisztrátor + field_last_login_on: Utolsó bejelentkezés + field_language: Nyelv + field_effective_date: Dátum + field_password: Jelszó + field_new_password: Új jelszó + field_password_confirmation: MegerÅ‘sítés + field_version: Verzió + field_type: Típus + field_host: Kiszolgáló + field_port: Port + field_account: Felhasználói fiók + field_base_dn: Base DN + field_attr_login: Bejelentkezési tulajdonság + field_attr_firstname: Keresztnév + field_attr_lastname: Vezetéknév + field_attr_mail: E-mail + field_onthefly: On-the-fly felhasználó létrehozás + field_start_date: Kezdés dátuma + field_done_ratio: Készültség (%) + field_auth_source: Azonosítási mód + field_hide_mail: Rejtse el az e-mail címem + field_comments: Megjegyzés + field_url: URL + field_start_page: KezdÅ‘lap + field_subproject: Alprojekt + field_hours: Óra + field_activity: Aktivitás + field_spent_on: Dátum + field_identifier: Azonosító + field_is_filter: SzűrÅ‘ként használható + field_issue_to: Kapcsolódó feladat + field_delay: Késés + field_assignable: Feladat rendelhetÅ‘ ehhez a szerepkörhöz + field_redirect_existing_links: LétezÅ‘ linkek átirányítása + field_estimated_hours: Becsült idÅ‘igény + field_column_names: Oszlopok + field_time_zone: IdÅ‘zóna + field_searchable: KereshetÅ‘ + field_default_value: Alapértelmezett érték + field_comments_sorting: Feljegyzések megjelenítése + + setting_app_title: Alkalmazás címe + setting_app_subtitle: Alkalmazás alcíme + setting_welcome_text: ÜdvözlÅ‘ üzenet + setting_default_language: Alapértelmezett nyelv + setting_login_required: Azonosítás szükséges + setting_self_registration: Regisztráció + setting_attachment_max_size: Melléklet max. mérete + setting_issues_export_limit: Feladatok exportálásának korlátja + setting_mail_from: Kibocsátó e-mail címe + setting_bcc_recipients: Titkos másolat címzet (bcc) + setting_host_name: Kiszolgáló neve + setting_text_formatting: Szöveg formázás + setting_wiki_compression: Wiki történet tömörítés + setting_feeds_limit: Atom tartalom korlát + setting_default_projects_public: Az új projektek alapértelmezés szerint nyilvánosak + setting_autofetch_changesets: Commitok automatikus lehúzása + setting_sys_api_enabled: WS engedélyezése a tárolók kezeléséhez + setting_commit_ref_keywords: Hivatkozó kulcsszavak + setting_commit_fix_keywords: Javítások kulcsszavai + setting_autologin: Automatikus bejelentkezés + setting_date_format: Dátum formátum + setting_time_format: IdÅ‘ formátum + setting_cross_project_issue_relations: Kereszt-projekt feladat hivatkozások engedélyezése + setting_issue_list_default_columns: Az alapértelmezésként megjelenített oszlopok a feladat listában + setting_emails_footer: E-mail lábléc + setting_protocol: Protokol + setting_per_page_options: Objektum / oldal opciók + setting_user_format: Felhasználók megjelenítésének formája + setting_activity_days_default: Napok megjelenítése a project aktivitásnál + setting_display_subprojects_issues: Alapértelmezettként mutassa az alprojektek feladatait is a projekteken + setting_start_of_week: A hét elsÅ‘ napja + + project_module_issue_tracking: Feladat követés + project_module_time_tracking: IdÅ‘ rögzítés + project_module_news: Hírek + project_module_documents: Dokumentumok + project_module_files: Fájlok + project_module_wiki: Wiki + project_module_repository: Forráskód + project_module_boards: Fórumok + + label_user: Felhasználó + label_user_plural: Felhasználók + label_user_new: Új felhasználó + label_project: Projekt + label_project_new: Új projekt + label_project_plural: Projektek + label_x_projects: + zero: nincsenek projektek + one: 1 projekt + other: "%{count} projekt" + label_project_all: Az összes projekt + label_project_latest: Legutóbbi projektek + label_issue: Feladat + label_issue_new: Új feladat + label_issue_plural: Feladatok + label_issue_view_all: Minden feladat + label_issues_by: "%{value} feladatai" + label_issue_added: Feladat hozzáadva + label_issue_updated: Feladat frissítve + label_document: Dokumentum + label_document_new: Új dokumentum + label_document_plural: Dokumentumok + label_document_added: Dokumentum hozzáadva + label_role: Szerepkör + label_role_plural: Szerepkörök + label_role_new: Új szerepkör + label_role_and_permissions: Szerepkörök, és jogosultságok + label_member: RésztvevÅ‘ + label_member_new: Új résztvevÅ‘ + label_member_plural: RésztvevÅ‘k + label_tracker: Feladat típus + label_tracker_plural: Feladat típusok + label_tracker_new: Új feladat típus + label_workflow: Workflow + label_issue_status: Feladat státusz + label_issue_status_plural: Feladat státuszok + label_issue_status_new: Új státusz + label_issue_category: Feladat kategória + label_issue_category_plural: Feladat kategóriák + label_issue_category_new: Új kategória + label_custom_field: Egyéni mezÅ‘ + label_custom_field_plural: Egyéni mezÅ‘k + label_custom_field_new: Új egyéni mezÅ‘ + label_enumerations: Felsorolások + label_enumeration_new: Új érték + label_information: Információ + label_information_plural: Információk + label_please_login: Jelentkezzen be + label_register: Regisztráljon + label_password_lost: Elfelejtett jelszó + label_home: KezdÅ‘lap + label_my_page: Saját kezdÅ‘lapom + label_my_account: Fiókom adatai + label_my_projects: Saját projektem + label_administration: Adminisztráció + label_login: Bejelentkezés + label_logout: Kijelentkezés + label_help: Súgó + label_reported_issues: Bejelentett feladatok + label_assigned_to_me_issues: A nekem kiosztott feladatok + label_last_login: Utolsó bejelentkezés + label_registered_on: Regisztrált + label_activity: Történések + label_overall_activity: Teljes aktivitás + label_new: Új + label_logged_as: Bejelentkezve, mint + label_environment: Környezet + label_authentication: Azonosítás + label_auth_source: Azonosítás módja + label_auth_source_new: Új azonosítási mód + label_auth_source_plural: Azonosítási módok + label_subproject_plural: Alprojektek + label_and_its_subprojects: "%{value} és alprojektjei" + label_min_max_length: Min - Max hossz + label_list: Lista + label_date: Dátum + label_integer: Egész + label_float: LebegÅ‘pontos + label_boolean: Logikai + label_string: Szöveg + label_text: Hosszú szöveg + label_attribute: Tulajdonság + label_attribute_plural: Tulajdonságok + label_no_data: Nincs megjeleníthetÅ‘ adat + label_change_status: Státusz módosítása + label_history: Történet + label_attachment: Fájl + label_attachment_new: Új fájl + label_attachment_delete: Fájl törlése + label_attachment_plural: Fájlok + label_file_added: Fájl hozzáadva + label_report: Jelentés + label_report_plural: Jelentések + label_news: Hírek + label_news_new: Hír hozzáadása + label_news_plural: Hírek + label_news_latest: Legutóbbi hírek + label_news_view_all: Minden hír megtekintése + label_news_added: Hír hozzáadva + label_settings: Beállítások + label_overview: Ãttekintés + label_version: Verzió + label_version_new: Új verzió + label_version_plural: Verziók + label_confirmation: Jóváhagyás + label_export_to: Exportálás + label_read: Olvas... + label_public_projects: Nyilvános projektek + label_open_issues: nyitott + label_open_issues_plural: nyitott + label_closed_issues: lezárt + label_closed_issues_plural: lezárt + label_x_open_issues_abbr_on_total: + zero: nyitott 0 / %{total} + one: nyitott 1 / %{total} + other: "nyitott %{count} / %{total}" + label_x_open_issues_abbr: + zero: 0 nyitott + one: 1 nyitott + other: "%{count} nyitott" + label_x_closed_issues_abbr: + zero: 0 lezárt + one: 1 lezárt + other: "%{count} lezárt" + label_total: Összesen + label_permissions: Jogosultságok + label_current_status: Jelenlegi státusz + label_new_statuses_allowed: Státusz változtatások engedélyei + label_all: mind + label_none: nincs + label_nobody: senki + label_next: KövetkezÅ‘ + label_previous: ElÅ‘zÅ‘ + label_used_by: Használja + label_details: Részletek + label_add_note: Jegyzet hozzáadása + label_per_page: Oldalanként + label_calendar: Naptár + label_months_from: hónap, kezdve + label_gantt: Gantt + label_internal: BelsÅ‘ + label_last_changes: "utolsó %{count} változás" + label_change_view_all: Minden változás megtekintése + label_personalize_page: Az oldal testreszabása + label_comment: Megjegyzés + label_comment_plural: Megjegyzés + label_x_comments: + zero: nincs megjegyzés + one: 1 megjegyzés + other: "%{count} megjegyzés" + label_comment_add: Megjegyzés hozzáadása + label_comment_added: Megjegyzés hozzáadva + label_comment_delete: Megjegyzések törlése + label_query: Egyéni lekérdezés + label_query_plural: Egyéni lekérdezések + label_query_new: Új lekérdezés + label_filter_add: SzűrÅ‘ hozzáadása + label_filter_plural: SzűrÅ‘k + label_equals: egyenlÅ‘ + label_not_equals: nem egyenlÅ‘ + label_in_less_than: kevesebb, mint + label_in_more_than: több, mint + label_in: in + label_today: ma + label_all_time: mindenkor + label_yesterday: tegnap + label_this_week: aktuális hét + label_last_week: múlt hét + label_last_n_days: "az elmúlt %{count} nap" + label_this_month: aktuális hónap + label_last_month: múlt hónap + label_this_year: aktuális év + label_date_range: Dátum intervallum + label_less_than_ago: kevesebb, mint nappal ezelÅ‘tt + label_more_than_ago: több, mint nappal ezelÅ‘tt + label_ago: nappal ezelÅ‘tt + label_contains: tartalmazza + label_not_contains: nem tartalmazza + label_day_plural: nap + label_repository: Forráskód + label_repository_plural: Forráskódok + label_browse: Tallóz + label_revision: Revízió + label_revision_plural: Revíziók + label_associated_revisions: Kapcsolt revíziók + label_added: hozzáadva + label_modified: módosítva + label_deleted: törölve + label_latest_revision: Legutolsó revízió + label_latest_revision_plural: Legutolsó revíziók + label_view_revisions: Revíziók megtekintése + label_max_size: Maximális méret + label_sort_highest: Az elejére + label_sort_higher: Eggyel feljebb + label_sort_lower: Eggyel lejjebb + label_sort_lowest: Az aljára + label_roadmap: Életút + label_roadmap_due_in: "Elkészültéig várhatóan még %{value}" + label_roadmap_overdue: "%{value} késésben" + label_roadmap_no_issues: Nincsenek feladatok ehhez a verzióhoz + label_search: Keresés + label_result_plural: Találatok + label_all_words: Minden szó + label_wiki: Wiki + label_wiki_edit: Wiki szerkesztés + label_wiki_edit_plural: Wiki szerkesztések + label_wiki_page: Wiki oldal + label_wiki_page_plural: Wiki oldalak + label_index_by_title: Cím szerint indexelve + label_index_by_date: Dátum szerint indexelve + label_current_version: Jelenlegi verzió + label_preview: ElÅ‘nézet + label_feed_plural: Visszajelzések + label_changes_details: Változások részletei + label_issue_tracking: Feladat követés + label_spent_time: Ráfordított idÅ‘ + label_f_hour: "%{value} óra" + label_f_hour_plural: "%{value} óra" + label_time_tracking: IdÅ‘ rögzítés + label_change_plural: Változások + label_statistics: Statisztikák + label_commits_per_month: Commitok havonta + label_commits_per_author: Commitok szerzÅ‘nként + label_view_diff: Különbségek megtekintése + label_diff_inline: soronként + label_diff_side_by_side: egymás mellett + label_options: Opciók + label_copy_workflow_from: Workflow másolása innen + label_permissions_report: Jogosultsági riport + label_watched_issues: Megfigyelt feladatok + label_related_issues: Kapcsolódó feladatok + label_applied_status: Alkalmazandó státusz + label_loading: Betöltés... + label_relation_new: Új kapcsolat + label_relation_delete: Kapcsolat törlése + label_relates_to: kapcsolódik + label_duplicates: duplikálja + label_blocks: zárolja + label_blocked_by: zárolta + label_precedes: megelÅ‘zi + label_follows: követi + label_end_to_start: végétÅ‘l indulásig + label_end_to_end: végétÅ‘l végéig + label_start_to_start: indulástól indulásig + label_start_to_end: indulástól végéig + label_stay_logged_in: Emlékezzen rám + label_disabled: kikapcsolva + label_show_completed_versions: A kész verziók mutatása + label_me: én + label_board: Fórum + label_board_new: Új fórum + label_board_plural: Fórumok + label_topic_plural: Témák + label_message_plural: Üzenetek + label_message_last: Utolsó üzenet + label_message_new: Új üzenet + label_message_posted: Üzenet hozzáadva + label_reply_plural: Válaszok + label_send_information: Fiók infomációk küldése a felhasználónak + label_year: Év + label_month: Hónap + label_week: Hét + label_date_from: 'Kezdet:' + label_date_to: 'Vége:' + label_language_based: A felhasználó nyelve alapján + label_sort_by: "%{value} szerint rendezve" + label_send_test_email: Teszt e-mail küldése + label_feeds_access_key_created_on: "Atom hozzáférési kulcs létrehozva %{value}" + label_module_plural: Modulok + label_added_time_by: "%{author} adta hozzá %{age}" + label_updated_time: "Utolsó módosítás %{value}" + label_jump_to_a_project: Ugrás projekthez... + label_file_plural: Fájlok + label_changeset_plural: Changesets + label_default_columns: Alapértelmezett oszlopok + label_no_change_option: (Nincs változás) + label_bulk_edit_selected_issues: A kiválasztott feladatok kötegelt szerkesztése + label_theme: Téma + label_default: Alapértelmezett + label_search_titles_only: Keresés csak a címekben + label_user_mail_option_all: "Minden eseményrÅ‘l minden saját projektemben" + label_user_mail_option_selected: "Minden eseményrÅ‘l a kiválasztott projektekben..." + label_user_mail_no_self_notified: "Nem kérek értesítést az általam végzett módosításokról" + label_registration_activation_by_email: Fiók aktiválása e-mailben + label_registration_manual_activation: Manuális fiók aktiválás + label_registration_automatic_activation: Automatikus fiók aktiválás + label_display_per_page: "Oldalanként: %{value}" + label_age: Kor + label_change_properties: Tulajdonságok változtatása + label_general: Ãltalános + label_more: továbbiak + label_scm: SCM + label_plugins: Pluginek + label_ldap_authentication: LDAP azonosítás + label_downloads_abbr: D/L + label_optional_description: Opcionális leírás + label_add_another_file: Újabb fájl hozzáadása + label_preferences: Tulajdonságok + label_chronological_order: IdÅ‘rendben + label_reverse_chronological_order: Fordított idÅ‘rendben + label_planning: Tervezés + + button_login: Bejelentkezés + button_submit: Elfogad + button_save: Mentés + button_check_all: Mindent kijelöl + button_uncheck_all: Kijelölés törlése + button_delete: Töröl + button_create: Létrehoz + button_test: Teszt + button_edit: Szerkeszt + button_add: Hozzáad + button_change: Változtat + button_apply: Alkalmaz + button_clear: Töröl + button_lock: Zárol + button_unlock: Felold + button_download: Letöltés + button_list: Lista + button_view: Megnéz + button_move: Mozgat + button_back: Vissza + button_cancel: Mégse + button_activate: Aktivál + button_sort: Rendezés + button_log_time: IdÅ‘ rögzítés + button_rollback: Visszaáll erre a verzióra + button_watch: Megfigyel + button_unwatch: Megfigyelés törlése + button_reply: Válasz + button_archive: Archivál + button_unarchive: Dearchivál + button_reset: Reset + button_rename: Ãtnevez + button_change_password: Jelszó megváltoztatása + button_copy: Másol + button_annotate: Jegyzetel + button_update: Módosít + button_configure: Konfigurál + + status_active: aktív + status_registered: regisztrált + status_locked: zárolt + + text_select_mail_notifications: Válasszon eseményeket, amelyekrÅ‘l e-mail értesítést kell küldeni. + text_regexp_info: pl. ^[A-Z0-9]+$ + text_min_max_length_info: 0 = nincs korlátozás + text_project_destroy_confirmation: Biztosan törölni szeretné a projektet és vele együtt minden kapcsolódó adatot ? + text_subprojects_destroy_warning: "Az alprojekt(ek): %{value} szintén törlésre kerülnek." + text_workflow_edit: Válasszon egy szerepkört, és egy feladat típust a workflow szerkesztéséhez + text_are_you_sure: Biztos benne ? + text_tip_issue_begin_day: a feladat ezen a napon kezdÅ‘dik + text_tip_issue_end_day: a feladat ezen a napon ér véget + text_tip_issue_begin_end_day: a feladat ezen a napon kezdÅ‘dik és ér véget + text_caracters_maximum: "maximum %{count} karakter." + text_caracters_minimum: "Legkevesebb %{count} karakter hosszúnek kell lennie." + text_length_between: "Legalább %{min} és legfeljebb %{max} hosszú karakter." + text_tracker_no_workflow: Nincs workflow definiálva ehhez a feladat típushoz + text_unallowed_characters: Tiltott karakterek + text_comma_separated: Több érték megengedett (vesszÅ‘vel elválasztva) + text_issues_ref_in_commit_messages: Hivatkozás feladatokra, feladatok javítása a commit üzenetekben + text_issue_added: "%{author} új feladatot hozott létre %{id} sorszámmal." + text_issue_updated: "%{author} módosította a %{id} sorszámú feladatot." + text_wiki_destroy_confirmation: Biztosan törölni szeretné ezt a wiki-t minden tartalmával együtt ? + text_issue_category_destroy_question: "Néhány feladat (%{count}) hozzá van rendelve ehhez a kategóriához. Mit szeretne tenni?" + text_issue_category_destroy_assignments: Kategória hozzárendelés megszüntetése + text_issue_category_reassign_to: Feladatok újra hozzárendelése másik kategóriához + text_user_mail_option: "A nem kiválasztott projektekrÅ‘l csak akkor kap értesítést, ha figyelést kér rá, vagy részt vesz benne (pl. Ön a létrehozó, vagy a hozzárendelÅ‘)" + text_no_configuration_data: "Szerepkörök, feladat típusok, feladat státuszok, és workflow adatok még nincsenek konfigurálva.\nErÅ‘sen ajánlott, az alapértelmezett konfiguráció betöltése, és utána módosíthatja azt." + text_load_default_configuration: Alapértelmezett konfiguráció betöltése + text_status_changed_by_changeset: "Applied in changeset %{value}." + text_issues_destroy_confirmation: 'Biztos benne, hogy törölni szeretné a kijelölt feladato(ka)t ?' + text_select_project_modules: 'Válassza ki az engedélyezett modulokat ehhez a projekthez:' + text_default_administrator_account_changed: Alapértelmezett adminisztrátor fiók megváltoztatva + text_file_repository_writable: Fájl tároló írható + text_rmagick_available: RMagick elérhetÅ‘ (nem kötelezÅ‘) + text_destroy_time_entries_question: "%{hours} órányi munka van rögzítve a feladatokon, amiket törölni szeretne. Mit szeretne tenni?" + text_destroy_time_entries: A rögzített órák törlése + text_assign_time_entries_to_project: A rögzített órák hozzárendelése a projekthez + text_reassign_time_entries: 'A rögzített órák újra hozzárendelése másik feladathoz:' + + default_role_manager: VezetÅ‘ + default_role_developer: FejlesztÅ‘ + default_role_reporter: BejelentÅ‘ + default_tracker_bug: Hiba + default_tracker_feature: Fejlesztés + default_tracker_support: Támogatás + default_issue_status_new: Új + default_issue_status_in_progress: Folyamatban + default_issue_status_resolved: Megoldva + default_issue_status_feedback: Visszajelzés + default_issue_status_closed: Lezárt + default_issue_status_rejected: Elutasított + default_doc_category_user: Felhasználói dokumentáció + default_doc_category_tech: Technikai dokumentáció + default_priority_low: Alacsony + default_priority_normal: Normál + default_priority_high: Magas + default_priority_urgent: SürgÅ‘s + default_priority_immediate: Azonnal + default_activity_design: Tervezés + default_activity_development: Fejlesztés + + enumeration_issue_priorities: Feladat prioritások + enumeration_doc_categories: Dokumentum kategóriák + enumeration_activities: Tevékenységek (idÅ‘ rögzítés) + mail_body_reminder: "%{count} neked kiosztott feladat határidÅ‘s az elkövetkezÅ‘ %{days} napban:" + mail_subject_reminder: "%{count} feladat határidÅ‘s az elkövetkezÅ‘ %{days} napban" + text_user_wrote: "%{value} írta:" + label_duplicated_by: duplikálta + setting_enabled_scm: ForráskódkezelÅ‘ (SCM) engedélyezése + text_enumeration_category_reassign_to: 'Újra hozzárendelés ehhez:' + text_enumeration_destroy_question: "%{count} objektum van hozzárendelve ehhez az értékhez." + label_incoming_emails: Beérkezett levelek + label_generate_key: Kulcs generálása + setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez + setting_mail_handler_api_key: API kulcs + text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesítések ki vannak kapcsolva.\nÃllítsd be az SMTP szervert a config/configuration.yml fájlban és indítsd újra az alkalmazást, hogy érvénybe lépjen." + field_parent_title: SzülÅ‘ oldal + label_issue_watchers: MegfigyelÅ‘k + button_quote: Hozzászólás / Idézet + setting_sequential_project_identifiers: Szekvenciális projekt azonosítók generálása + notice_unable_delete_version: A verziót nem lehet törölni + label_renamed: átnevezve + label_copied: lemásolva + setting_plain_text_mail: csak szöveg (nem HTML) + permission_view_files: Fájlok megtekintése + permission_edit_issues: Feladatok szerkesztése + permission_edit_own_time_entries: Saját idÅ‘napló szerkesztése + permission_manage_public_queries: Nyilvános kérések kezelése + permission_add_issues: Feladat felvétele + permission_log_time: IdÅ‘ rögzítése + permission_view_changesets: Változáskötegek megtekintése + permission_view_time_entries: IdÅ‘rögzítések megtekintése + permission_manage_versions: Verziók kezelése + permission_manage_wiki: Wiki kezelése + permission_manage_categories: Feladat kategóriák kezelése + permission_protect_wiki_pages: Wiki oldalak védelme + permission_comment_news: Hírek kommentelése + permission_delete_messages: Üzenetek törlése + permission_select_project_modules: Projekt modulok kezelése + permission_edit_wiki_pages: Wiki oldalak szerkesztése + permission_add_issue_watchers: MegfigyelÅ‘k felvétele + permission_view_gantt: Gannt diagramm megtekintése + permission_move_issues: Feladatok mozgatása + permission_manage_issue_relations: Feladat kapcsolatok kezelése + permission_delete_wiki_pages: Wiki oldalak törlése + permission_manage_boards: Fórumok kezelése + permission_delete_wiki_pages_attachments: Csatolmányok törlése + permission_view_wiki_edits: Wiki történet megtekintése + permission_add_messages: Üzenet beküldése + permission_view_messages: Üzenetek megtekintése + permission_manage_files: Fájlok kezelése + permission_edit_issue_notes: Jegyzetek szerkesztése + permission_manage_news: Hírek kezelése + permission_view_calendar: Naptár megtekintése + permission_manage_members: Tagok kezelése + permission_edit_messages: Üzenetek szerkesztése + permission_delete_issues: Feladatok törlése + permission_view_issue_watchers: MegfigyelÅ‘k listázása + permission_manage_repository: Tárolók kezelése + permission_commit_access: Commit hozzáférés + permission_browse_repository: Tároló böngészése + permission_view_documents: Dokumetumok megtekintése + permission_edit_project: Projekt szerkesztése + permission_add_issue_notes: Jegyzet rögzítése + permission_save_queries: Kérések mentése + permission_view_wiki_pages: Wiki megtekintése + permission_rename_wiki_pages: Wiki oldalak átnevezése + permission_edit_time_entries: IdÅ‘naplók szerkesztése + permission_edit_own_issue_notes: Saját jegyzetek szerkesztése + setting_gravatar_enabled: Felhasználói fényképek engedélyezése + label_example: Példa + text_repository_usernames_mapping: "Ãllítsd be a felhasználó összerendeléseket a Redmine, és a tároló logban található felhasználók között.\nAz azonos felhasználó nevek összerendelése automatikusan megtörténik." + permission_edit_own_messages: Saját üzenetek szerkesztése + permission_delete_own_messages: Saját üzenetek törlése + label_user_activity: "%{value} tevékenységei" + label_updated_time_by: "Módosította %{author} %{age}" + text_diff_truncated: '... A diff fájl vége nem jelenik meg, mert hosszab, mint a megjeleníthetÅ‘ sorok száma.' + setting_diff_max_lines_displayed: A megjelenítendÅ‘ sorok száma (maximum) a diff fájloknál + text_plugin_assets_writable: Plugin eszközök könyvtár írható + warning_attachments_not_saved: "%{count} fájl mentése nem sikerült." + button_create_and_continue: Létrehozás és folytatás + text_custom_field_possible_values_info: 'Értékenként egy sor' + label_display: Megmutat + field_editable: SzerkeszthetÅ‘ + setting_repository_log_display_limit: Maximum hány revíziót mutasson meg a log megjelenítésekor + setting_file_max_size_displayed: Maximum mekkora szövegfájlokat jelenítsen meg soronkénti összehasonlításnál + field_watcher: MegfigyelÅ‘ + setting_openid: OpenID regisztráció és bejelentkezés engedélyezése + field_identity_url: OpenID URL + label_login_with_open_id_option: bejelentkezés OpenID használatával + field_content: Tartalom + label_descending: CsökkenÅ‘ + label_sort: Rendezés + label_ascending: NövekvÅ‘ + label_date_from_to: "%{start} -tól %{end} -ig" + label_greater_or_equal: ">=" + label_less_or_equal: "<=" + text_wiki_page_destroy_question: Ennek az oldalnak %{descendants} gyermek-, és leszármazott oldala van. Mit szeretne tenni? + text_wiki_page_reassign_children: Aloldalak hozzárendelése ehhez a szülÅ‘ oldalhoz + text_wiki_page_nullify_children: Aloldalak átalakítása fÅ‘oldallá + text_wiki_page_destroy_children: Minden aloldal és leszármazottjának törlése + setting_password_min_length: Minimum jelszó hosszúság + field_group_by: Szerint csoportosítva + mail_subject_wiki_content_updated: "'%{id}' wiki oldal frissítve" + label_wiki_content_added: Wiki oldal hozzáadva + mail_subject_wiki_content_added: "Új wiki oldal: '%{id}'" + mail_body_wiki_content_added: "%{author} létrehozta a '%{id}' wiki oldalt." + label_wiki_content_updated: Wiki oldal frissítve + mail_body_wiki_content_updated: "%{author} frissítette a '%{id}' wiki oldalt." + permission_add_project: Projekt létrehozása + setting_new_project_user_role_id: Projekt létrehozási jog nem adminisztrátor felhasználóknak + label_view_all_revisions: Összes verzió + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: Nincs feladat típus hozzárendelve ehhez a projekthez. Kérem ellenÅ‘rizze a projekt beállításait. + error_no_default_issue_status: Nincs alapértelmezett feladat státusz beállítva. Kérem ellenÅ‘rizze a beállításokat (Itt találja "Adminisztráció -> Feladat státuszok"). + text_journal_changed: "%{label} megváltozott, %{old} helyett %{new} lett" + text_journal_set_to: "%{label} új értéke: %{value}" + text_journal_deleted: "%{label} törölve lett (%{old})" + label_group_plural: Csoportok + label_group: Csoport + label_group_new: Új csoport + label_time_entry_plural: IdÅ‘ráfordítás + text_journal_added: "%{label} %{value} hozzáadva" + field_active: Aktív + enumeration_system_activity: Rendszertevékenység + permission_delete_issue_watchers: MegfigyelÅ‘k törlése + version_status_closed: lezárt + version_status_locked: zárolt + version_status_open: nyitott + error_can_not_reopen_issue_on_closed_version: Lezárt verzióhoz rendelt feladatot nem lehet újranyitni + label_user_anonymous: Névtelen + button_move_and_follow: Mozgatás és követés + setting_default_projects_modules: Alapértelmezett modulok az új projektekhez + setting_gravatar_default: Alapértelmezett Gravatar kép + field_sharing: Megosztás + label_version_sharing_hierarchy: Projekt hierarchiával + label_version_sharing_system: Minden projekttel + label_version_sharing_descendants: Alprojektekkel + label_version_sharing_tree: Projekt fával + label_version_sharing_none: Nincs megosztva + error_can_not_archive_project: A projektet nem lehet archiválni + button_duplicate: Duplikálás + button_copy_and_follow: Másolás és követés + label_copy_source: Forrás + setting_issue_done_ratio: Feladat készültségi szint számolása a következÅ‘ alapján + setting_issue_done_ratio_issue_status: Feladat státusz alapján + error_issue_done_ratios_not_updated: A feladat készültségi szintek nem lettek frissítve. + error_workflow_copy_target: Kérem válasszon cél feladat típus(oka)t és szerepkör(öke)t. + setting_issue_done_ratio_issue_field: A feladat mezÅ‘ alapján + label_copy_same_as_target: A céllal egyezÅ‘ + label_copy_target: Cél + notice_issue_done_ratios_updated: Feladat készültségi szintek frissítve. + error_workflow_copy_source: Kérem válasszon forrás feladat típust vagy szerepkört + label_update_issue_done_ratios: Feladat készültségi szintek frissítése + setting_start_of_week: A hét elsÅ‘ napja + permission_view_issues: Feladatok megtekintése + label_display_used_statuses_only: Csak olyan feladat státuszok megjelenítése, amit ez a feladat típus használ + label_revision_id: Revízió %{value} + label_api_access_key: API hozzáférési kulcs + label_api_access_key_created_on: API hozzáférési kulcs létrehozva %{value} ezelÅ‘tt + label_feeds_access_key: Atom hozzáférési kulcs + notice_api_access_key_reseted: Az API hozzáférési kulcsa újragenerálva. + setting_rest_api_enabled: REST web service engedélyezése + label_missing_api_access_key: Egy API hozzáférési kulcs hiányzik + label_missing_feeds_access_key: Atom hozzáférési kulcs hiányzik + button_show: Megmutat + text_line_separated: Több érték megadása lehetséges (soronként 1 érték). + setting_mail_handler_body_delimiters: E-mailek levágása a következÅ‘ sorok valamelyike esetén + permission_add_subprojects: Alprojektek létrehozása + label_subproject_new: Új alprojekt + text_own_membership_delete_confirmation: |- + Arra készül, hogy eltávolítja egyes vagy minden jogosultságát! Ezt követÅ‘en lehetséges, hogy nem fogja tudni szerkeszteni ezt a projektet! + Biztosan folyatni szeretné? + label_close_versions: Kész verziók lezárása + label_board_sticky: Sticky + setting_cache_formatted_text: Formázott szöveg gyorsítótárazása (Cache) + permission_export_wiki_pages: Wiki oldalak exportálása + permission_manage_project_activities: Projekt tevékenységek kezelése + label_board_locked: Zárolt + error_can_not_delete_custom_field: Nem lehet törölni az egyéni mezÅ‘t + permission_manage_subtasks: Alfeladatok kezelése + label_profile: Profil + error_unable_to_connect: Nem lehet csatlakozni (%{value}) + error_can_not_remove_role: Ez a szerepkör használatban van és ezért nem törölhetÅ‘- + field_parent_issue: SzülÅ‘ feladat + error_unable_delete_issue_status: Nem lehet törölni a feladat állapotát + label_subtask_plural: Alfeladatok + error_can_not_delete_tracker: Ebbe a kategóriába feladatok tartoznak és ezért nem törölhetÅ‘. + label_project_copy_notifications: Küldjön e-mail értesítéseket projektmásolás közben. + field_principal: FelelÅ‘s + label_my_page_block: Saját kezdÅ‘lap-blokk + notice_failed_to_save_members: "Nem sikerült menteni a tago(ka)t: %{errors}." + text_zoom_out: Kicsinyít + text_zoom_in: Nagyít + notice_unable_delete_time_entry: Az idÅ‘rögzítés nem törölhetÅ‘ + label_overall_spent_time: Összes ráfordított idÅ‘ + field_time_entries: IdÅ‘ rögzítés + project_module_gantt: Gantt + project_module_calendar: Naptár + button_edit_associated_wikipage: "Hozzárendelt Wiki oldal szerkesztése: %{page_title}" + field_text: Szöveg mezÅ‘ + label_user_mail_option_only_owner: Csak arról, aminek én vagyok a tulajdonosa + setting_default_notification_option: Alapértelmezett értesítési beállítások + label_user_mail_option_only_my_events: Csak az általam megfigyelt dolgokról vagy amiben részt veszek + label_user_mail_option_only_assigned: Csak a hozzámrendelt dolgokról + label_user_mail_option_none: Semilyen eseményrÅ‘l + field_member_of_group: Hozzárendelt csoport + field_assigned_to_role: Hozzárendelt szerepkör + notice_not_authorized_archived_project: A projekt, amihez hozzá szeretnél férni archiválva lett. + label_principal_search: "Felhasználó vagy csoport keresése:" + label_user_search: "Felhasználó keresése:" + field_visible: Látható + setting_emails_header: Emailek fejléce + setting_commit_logtime_activity_id: A rögzített idÅ‘höz tartozó tevékenység + text_time_logged_by_changeset: Alkalmazva a %{value} changeset-ben. + setting_commit_logtime_enabled: IdÅ‘rögzítés engedélyezése + notice_gantt_chart_truncated: A diagram le lett vágva, mert elérte a maximálisan megjeleníthetÅ‘ elemek számát (%{max}) + setting_gantt_items_limit: A gantt diagrammon megjeleníthetÅ‘ maximális elemek száma + field_warn_on_leaving_unsaved: Figyelmeztessen, nem mentett módosításokat tartalmazó oldal elhagyásakor + text_warn_on_leaving_unsaved: A jelenlegi oldal nem mentett módosításokat tartalmaz, ami elvész, ha elhagyja az oldalt. + label_my_queries: Egyéni lekérdezéseim + text_journal_changed_no_detail: "%{label} módosítva" + label_news_comment_added: Megjegyzés hozzáadva a hírhez + button_expand_all: Mindet kibont + button_collapse_all: Mindet összecsuk + label_additional_workflow_transitions_for_assignee: További átmenetek engedélyezettek, ha a felhasználó a hozzárendelt + label_additional_workflow_transitions_for_author: További átmenetek engedélyezettek, ha a felhasználó a szerzÅ‘ + label_bulk_edit_selected_time_entries: A kiválasztott idÅ‘ bejegyzések csoportos szerkesztése + text_time_entries_destroy_confirmation: Biztos benne, hogy törölni szeretné a kiválasztott idÅ‘ bejegyzés(eke)t? + label_role_anonymous: Anonymous + label_role_non_member: Nem tag + label_issue_note_added: Jegyzet hozzáadva + label_issue_status_updated: Ãllapot módosítva + label_issue_priority_updated: Prioritás módosítva + label_issues_visibility_own: A felhasználó által létrehozott vagy hozzárendelt feladatok + field_issues_visibility: Feladatok láthatósága + label_issues_visibility_all: Minden feladat + permission_set_own_issues_private: Saját feladatok beállítása nyilvánosra vagy privátra + field_is_private: Privát + permission_set_issues_private: Feladatok beállítása nyilvánosra vagy privátra + label_issues_visibility_public: Minden nem privát feladat + text_issues_destroy_descendants_confirmation: Ezzel törölni fog %{count} alfeladatot is. + field_commit_logs_encoding: Commit üzenetek kódlapja + field_scm_path_encoding: Elérési útvonal kódlapja + text_scm_path_encoding_note: "Alapértelmezett: UTF-8" + field_path_to_repository: A repository elérési útja + field_root_directory: Gyökér könyvtár + field_cvs_module: Modul + field_cvsroot: CVSROOT + text_mercurial_repository_note: Helyi repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Parancs + text_scm_command_version: Verzió + 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 feladat + one: 1 feladat + other: "%{count} feladatok" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: mind + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Alprojektekkel + label_cross_project_tree: Projekt fával + label_cross_project_hierarchy: Projekt hierarchiával + label_cross_project_system: Minden projekttel + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Összesen + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/077e480273d388c124ae7826c285eaf11442be18.svn-base --- a/.svn/pristine/07/077e480273d388c124ae7826c285eaf11442be18.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -<% roles = Role.find_all_givable %> -<% projects = Project.active.find(:all, :order => 'lft') %> - -
    -<% if @group.memberships.any? %> - - - - - - - - <% @group.memberships.each do |membership| %> - <% next if membership.new_record? %> - - - - - -<% end; reset_cycle %> - -
    <%= l(:label_project) %><%= l(:label_role_plural) %>
    <%=h membership.project %> - <%=h membership.roles.sort.collect(&:to_s).join(', ') %> - <%= form_for(:membership, :remote => true, - :url => { :action => 'edit_membership', :id => @group, :membership_id => membership }, - :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %> -

    <% roles.each do |role| %> -
    - <% end %>

    -

    <%= submit_tag l(:button_change) %> - <%= link_to_function( - l(:button_cancel), - "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;" - ) %>

    - <% end %> -
    - <%= link_to_function( - l(:button_edit), - "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;", - :class => 'icon icon-edit' - ) %> - <%= delete_link({:controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership}, - :remote => true, - :method => :post) %> -
    -<% else %> -

    <%= l(:label_no_data) %>

    -<% end %> -
    - -
    -<% if projects.any? %> -
    <%=l(:label_project_new)%> -<%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %> -<%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %> -<%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %> -

    <%= l(:label_role_plural) %>: -<% roles.each do |role| %> - -<% end %>

    -

    <%= submit_tag l(:button_add) %>

    -<% end %> -
    -<% end %> -
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/078a4373b77072983b303eb90be2e921eb7adcfd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/078a4373b77072983b303eb90be2e921eb7adcfd.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,165 @@ +--- +queries_001: + id: 1 + type: IssueQuery + project_id: 1 + visibility: 2 + name: Multiple custom fields query + filters: | + --- + cf_1: + :values: + - MySQL + :operator: "=" + status_id: + :values: + - "1" + :operator: o + cf_2: + :values: + - "125" + :operator: "=" + + user_id: 1 + column_names: +queries_002: + id: 2 + type: IssueQuery + project_id: 1 + visibility: 0 + name: Private query for cookbook + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + status_id: + :values: + - "1" + :operator: o + + user_id: 3 + column_names: +queries_003: + id: 3 + type: IssueQuery + project_id: + visibility: 0 + name: Private query for all projects + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 3 + column_names: +queries_004: + id: 4 + type: IssueQuery + project_id: + visibility: 2 + name: Public query for all projects + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 2 + column_names: +queries_005: + id: 5 + type: IssueQuery + project_id: + visibility: 2 + name: Open issues by priority and tracker + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + sort_criteria: | + --- + - - priority + - desc + - - tracker + - asc +queries_006: + id: 6 + type: IssueQuery + project_id: + visibility: 2 + name: Open issues grouped by tracker + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + group_by: tracker + sort_criteria: | + --- + - - priority + - desc +queries_007: + id: 7 + type: IssueQuery + project_id: 2 + visibility: 2 + name: Public query for project 2 + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 2 + column_names: +queries_008: + id: 8 + type: IssueQuery + project_id: 2 + visibility: 0 + name: Private query for project 2 + filters: | + --- + tracker_id: + :values: + - "3" + :operator: "=" + + user_id: 2 + column_names: +queries_009: + id: 9 + type: IssueQuery + project_id: + visibility: 2 + name: Open issues grouped by list custom field + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + group_by: cf_1 + sort_criteria: | + --- + - - priority + - desc + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/07/07f4ae691a86068505e5dcb35963f143e7c54566.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/07f4ae691a86068505e5dcb35963f143e7c54566.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,30 @@ +

    +<%= 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 d98d22a98252 -r fb9a13467253 .svn/pristine/08/081cb3c9d1cc0a40761d7f17097c046e4db924a7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/081cb3c9d1cc0a40761d7f17097c046e4db924a7.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,264 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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('../base', __FILE__) + +class Redmine::UiTest::IssuesTest < Redmine::UiTest::Base + fixtures :projects, :users, :roles, :members, :member_roles, + :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues, + :enumerations, :custom_fields, :custom_values, :custom_fields_trackers, + :watchers + + def test_create_issue + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + within('form#issue-form') do + select 'Bug', :from => 'Tracker' + select 'Low', :from => 'Priority' + fill_in 'Subject', :with => 'new test issue' + fill_in 'Description', :with => 'new issue' + select '0 %', :from => 'Done' + fill_in 'Due date', :with => '' + fill_in 'Searchable field', :with => 'Value for field 2' + # click_button 'Create' would match both 'Create' and 'Create and continue' buttons + find('input[name=commit]').click + end + + # find created issue + issue = Issue.find_by_subject("new test issue") + assert_kind_of Issue, issue + + # check redirection + find 'div#flash_notice', :visible => true, :text => "Issue \##{issue.id} created." + assert_equal issue_path(:id => issue), current_path + + # check issue attributes + assert_equal 'jsmith', issue.author.login + assert_equal 1, issue.project.id + assert_equal IssueStatus.find_by_name('New'), issue.status + assert_equal Tracker.find_by_name('Bug'), issue.tracker + assert_equal IssuePriority.find_by_name('Low'), issue.priority + assert_equal 'Value for field 2', issue.custom_field_value(CustomField.find_by_name('Searchable field')) + end + + def test_create_issue_with_form_update + field1 = IssueCustomField.create!( + :field_format => 'string', + :name => 'Field1', + :is_for_all => true, + :trackers => Tracker.find_all_by_id([1, 2]) + ) + field2 = IssueCustomField.create!( + :field_format => 'string', + :name => 'Field2', + :is_for_all => true, + :trackers => Tracker.find_all_by_id(2) + ) + + Role.non_member.add_permission! :add_issues + Role.non_member.remove_permission! :edit_issues, :add_issue_notes + + log_user('someone', 'foo') + visit '/projects/ecookbook/issues/new' + assert page.has_no_content?(field2.name) + assert page.has_content?(field1.name) + + fill_in 'Subject', :with => 'New test issue' + fill_in 'Description', :with => 'New test issue description' + fill_in field1.name, :with => 'CF1 value' + select 'Low', :from => 'Priority' + + # field2 should show up when changing tracker + select 'Feature request', :from => 'Tracker' + assert page.has_content?(field2.name) + assert page.has_content?(field1.name) + + fill_in field2.name, :with => 'CF2 value' + assert_difference 'Issue.count' do + page.first(:button, 'Create').click + end + + issue = Issue.order('id desc').first + assert_equal 'New test issue', issue.subject + assert_equal 'New test issue description', issue.description + assert_equal 'Low', issue.priority.name + assert_equal 'CF1 value', issue.custom_field_value(field1) + assert_equal 'CF2 value', issue.custom_field_value(field2) + end + + def test_create_issue_with_watchers + user = User.generate!(:firstname => 'Some', :lastname => 'Watcher') + assert_equal 'Some Watcher', user.name + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + fill_in 'Subject', :with => 'Issue with watchers' + # Add a project member as watcher + check 'Dave Lopper' + # Search for another user + assert page.has_no_css?('form#new-watcher-form') + assert page.has_no_content?('Some Watcher') + click_link 'Search for watchers to add' + within('form#new-watcher-form') do + assert page.has_content?('Some One') + fill_in 'user_search', :with => 'watch' + assert page.has_no_content?('Some One') + check 'Some Watcher' + click_button 'Add' + end + assert page.has_css?('form#issue-form') + assert page.has_css?('p#watchers_form') + using_wait_time(30) do + within('span#watchers_inputs') do + within("label#issue_watcher_user_ids_#{user.id}") do + assert has_content?('Some Watcher'), "No watcher content" + end + end + end + assert_difference 'Issue.count' do + find('input[name=commit]').click + end + + issue = Issue.order('id desc').first + assert_equal ['Dave Lopper', 'Some Watcher'], issue.watcher_users.map(&:name).sort + end + + def test_create_issue_start_due_date + with_settings :default_issue_start_date_to_creation_date => 0 do + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + assert_equal "", page.find('input#issue_start_date').value + assert_equal "", page.find('input#issue_due_date').value + page.first('p#start_date_area img').click + page.first("td.ui-datepicker-days-cell-over a").click + assert_equal Date.today.to_s, page.find('input#issue_start_date').value + page.first('p#due_date_area img').click + page.first("td.ui-datepicker-days-cell-over a").click + assert_equal Date.today.to_s, page.find('input#issue_due_date').value + end + end + + def test_create_issue_start_due_date_default + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + fill_in 'Start date', :with => '2012-04-01' + fill_in 'Due date', :with => '' + page.first('p#due_date_area img').click + page.first("td.ui-datepicker-days-cell-over a").click + assert_equal '2012-04-01', page.find('input#issue_due_date').value + + fill_in 'Start date', :with => '' + fill_in 'Due date', :with => '2012-04-01' + page.first('p#start_date_area img').click + page.first("td.ui-datepicker-days-cell-over a").click + assert_equal '2012-04-01', page.find('input#issue_start_date').value + end + + def test_preview_issue_description + log_user('jsmith', 'jsmith') + visit '/projects/ecookbook/issues/new' + within('form#issue-form') do + fill_in 'Subject', :with => 'new issue subject' + fill_in 'Description', :with => 'new issue description' + click_link 'Preview' + end + find 'div#preview fieldset', :visible => true, :text => 'new issue description' + assert_difference 'Issue.count' do + find('input[name=commit]').click + end + + issue = Issue.order('id desc').first + assert_equal 'new issue description', issue.description + end + + def test_update_issue_with_form_update + field = IssueCustomField.create!( + :field_format => 'string', + :name => 'Form update CF', + :is_for_all => true, + :trackers => Tracker.find_all_by_name('Feature request') + ) + + Role.non_member.add_permission! :edit_issues + Role.non_member.remove_permission! :add_issues, :add_issue_notes + + log_user('someone', 'foo') + visit '/issues/1' + assert page.has_no_content?('Form update CF') + + page.first(:link, 'Update').click + # the custom field should show up when changing tracker + select 'Feature request', :from => 'Tracker' + assert page.has_content?('Form update CF') + + fill_in 'Form update', :with => 'CF value' + assert_no_difference 'Issue.count' do + page.first(:button, 'Submit').click + end + + issue = Issue.find(1) + assert_equal 'CF value', issue.custom_field_value(field) + end + + def test_remove_issue_watcher_from_sidebar + user = User.find(3) + Watcher.create!(:watchable => Issue.find(1), :user => user) + + log_user('jsmith', 'jsmith') + visit '/issues/1' + assert page.first('#sidebar').has_content?('Watchers (1)') + assert page.first('#sidebar').has_content?(user.name) + assert_difference 'Watcher.count', -1 do + page.first('ul.watchers .user-3 a.delete').click + assert page.first('#sidebar').has_content?('Watchers (0)') + end + assert page.first('#sidebar').has_no_content?(user.name) + end + + def test_watch_issue_via_context_menu + log_user('jsmith', 'jsmith') + visit '/issues' + assert page.has_css?('tr#issue-1') + find('tr#issue-1 td.updated_on').click + page.execute_script "$('tr#issue-1 td.updated_on').trigger('contextmenu');" + assert_difference 'Watcher.count' do + within('#context-menu') do + click_link 'Watch' + end + assert page.has_css?('tr#issue-1') + end + assert Issue.find(1).watched_by?(User.find_by_login('jsmith')) + end + + def test_bulk_watch_issues_via_context_menu + log_user('jsmith', 'jsmith') + visit '/issues' + assert page.has_css?('tr#issue-1') + assert page.has_css?('tr#issue-4') + find('tr#issue-1 input[type=checkbox]').click + find('tr#issue-4 input[type=checkbox]').click + page.execute_script "$('tr#issue-1 td.updated_on').trigger('contextmenu');" + assert_difference 'Watcher.count', 2 do + within('#context-menu') do + click_link 'Watch' + end + assert page.has_css?('tr#issue-1') + assert page.has_css?('tr#issue-4') + end + assert Issue.find(1).watched_by?(User.find_by_login('jsmith')) + assert Issue.find(4).watched_by?(User.find_by_login('jsmith')) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/08285e29f3d27f4d821cbe475ca426e39bd600c1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08285e29f3d27f4d821cbe475ca426e39bd600c1.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1115 @@ +th: + direction: ltr + 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: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] + abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] + abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "half a minute" + less_than_x_seconds: + one: "less than 1 second" + other: "less than %{count} seconds" + x_seconds: + one: "1 second" + other: "%{count} seconds" + less_than_x_minutes: + one: "less than a minute" + other: "less than %{count} minutes" + x_minutes: + one: "1 minute" + other: "%{count} minutes" + about_x_hours: + one: "about 1 hour" + other: "about %{count} hours" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 day" + other: "%{count} days" + about_x_months: + one: "about 1 month" + other: "about %{count} months" + x_months: + one: "1 month" + other: "%{count} months" + about_x_years: + one: "about 1 year" + other: "about %{count} years" + over_x_years: + one: "over 1 year" + other: "over %{count} years" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + precision: 3 + delimiter: "" + storage_units: + format: "%n %u" + units: + kb: KB + tb: TB + gb: GB + byte: + one: Byte + other: Bytes + mb: MB + +# Used in array.to_sentence. + support: + array: + sentence_connector: "and" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "ไม่อยู่ในรายà¸à¸²à¸£" + exclusion: "ถูà¸à¸ªà¸‡à¸§à¸™à¹„ว้" + invalid: "ไม่ถูà¸à¸•้อง" + confirmation: "พิมพ์ไม่เหมือนเดิม" + accepted: "ต้องยอมรับ" + empty: "ต้องเติม" + blank: "ต้องเติม" + too_long: "ยาวเà¸à¸´à¸™à¹„ป" + too_short: "สั้นเà¸à¸´à¸™à¹„ป" + wrong_length: "ความยาวไม่ถูà¸à¸•้อง" + taken: "ถูà¸à¹ƒà¸Šà¹‰à¹„ปà¹à¸¥à¹‰à¸§" + not_a_number: "ไม่ใช่ตัวเลข" + not_a_date: "ไม่ใช่วันที่ ที่ถูà¸à¸•้อง" + greater_than: "must be greater than %{count}" + greater_than_or_equal_to: "must be greater than or equal to %{count}" + equal_to: "must be equal to %{count}" + less_than: "must be less than %{count}" + less_than_or_equal_to: "must be less than or equal to %{count}" + odd: "must be odd" + even: "must be even" + greater_than_start_date: "ต้องมาà¸à¸à¸§à¹ˆà¸²à¸§à¸±à¸™à¹€à¸£à¸´à¹ˆà¸¡" + not_same_project: "ไม่ได้อยู่ในโครงà¸à¸²à¸£à¹€à¸”ียวà¸à¸±à¸™" + circular_dependency: "ความสัมพันธ์อ้างอิงเป็นวงà¸à¸¥à¸¡" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + 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: 'Thai (ไทย)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: Windows-874 + general_pdf_encoding: cp874 + general_first_day_of_week: '1' + + 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_email_sent: "อีเมล์ได้ถูà¸à¸ªà¹ˆà¸‡à¸–ึง %{value}" + notice_email_error: "เà¸à¸´à¸”ความผิดพลาดขณะà¸à¸³à¸ªà¹ˆà¸‡à¸­à¸µà¹€à¸¡à¸¥à¹Œ (%{value})" + notice_feeds_access_key_reseted: Atom access key ของคุณถูภreset à¹à¸¥à¹‰à¸§. + notice_failed_to_save_issues: "%{count} ปัà¸à¸«à¸²à¸ˆà¸²à¸ %{total} ปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸­à¸à¹„ม่สามารถจัดเà¸à¹‡à¸š: %{ids}." + notice_no_issue_selected: "ไม่มีปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸­à¸! à¸à¸£à¸¸à¸“าเลือà¸à¸›à¸±à¸à¸«à¸²à¸—ี่คุณต้องà¸à¸²à¸£à¹à¸à¹‰à¹„ข." + notice_account_pending: "บัà¸à¸Šà¸µà¸‚องคุณสร้างเสร็จà¹à¸¥à¹‰à¸§ ขณะนี้รอà¸à¸²à¸£à¸­à¸™à¸¸à¸¡à¸±à¸•ิจาà¸à¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£." + notice_default_data_loaded: ค่าเริ่มต้นโหลดเสร็จà¹à¸¥à¹‰à¸§. + + error_can_t_load_default_data: "ค่าเริ่มต้นโหลดไม่สำเร็จ: %{value}" + error_scm_not_found: "ไม่พบรุ่นที่ต้องà¸à¸²à¸£à¹ƒà¸™à¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ." + error_scm_command_failed: "เà¸à¸´à¸”ความผิดพลาดในà¸à¸²à¸£à¹€à¸‚้าถึงà¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ: %{value}" + error_scm_annotate: "entry ไม่มีอยู่จริง หรือไม่สามารถเขียนหมายเหตุประà¸à¸­à¸š." + error_issue_not_found_in_project: 'ไม่พบปัà¸à¸«à¸²à¸™à¸µà¹‰ หรือปัà¸à¸«à¸²à¹„ม่ได้อยู่ในโครงà¸à¸²à¸£à¸™à¸µà¹‰' + + 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}) ได้ลงทะเบียน. บัà¸à¸Šà¸µà¸‚องเขาà¸à¸³à¸¥à¸±à¸‡à¸£à¸­à¸­à¸™à¸¸à¸¡à¸±à¸•ิ:" + + + 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: Regular expression + 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_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: เข้าระบบ attribute + field_attr_firstname: ชื่อ attribute + field_attr_lastname: นามสà¸à¸¸à¸¥ attribute + field_attr_mail: อีเมล์ attribute + 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_zone: ย่านเวลา + field_searchable: ค้นหาได้ + field_default_value: ค่าเริ่มต้น + field_comments_sorting: à¹à¸ªà¸”งความเห็น + + 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_host_name: ชื่อโฮสต์ + setting_text_formatting: à¸à¸²à¸£à¸ˆà¸±à¸”รูปà¹à¸šà¸šà¸‚้อความ + setting_wiki_compression: บีบอัดประวัติ Wiki + setting_feeds_limit: จำนวน Feed + setting_default_projects_public: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆà¸¡à¸µà¸„่าเริ่มต้นเป็น สาธารณะ + setting_autofetch_changesets: ดึง commits อัตโนมัติ + setting_sys_api_enabled: เปิดใช้ WS สำหรับà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸—ี่เà¸à¹‡à¸šà¸•้นฉบับ + setting_commit_ref_keywords: คำสำคัภReferencing + setting_commit_fix_keywords: คำสำคัภFixing + setting_autologin: เข้าระบบอัตโนมัติ + setting_date_format: รูปà¹à¸šà¸šà¸§à¸±à¸™à¸—ี่ + setting_time_format: รูปà¹à¸šà¸šà¹€à¸§à¸¥à¸² + setting_cross_project_issue_relations: อนุà¸à¸²à¸•ให้ระบุปัà¸à¸«à¸²à¸‚้ามโครงà¸à¸²à¸£ + setting_issue_list_default_columns: สดมภ์เริ่มต้นà¹à¸ªà¸”งในรายà¸à¸²à¸£à¸›à¸±à¸à¸«à¸² + setting_emails_footer: คำลงท้ายอีเมล์ + setting_protocol: Protocol + setting_per_page_options: ตัวเลือà¸à¸ˆà¸³à¸™à¸§à¸™à¸•่อหน้า + setting_user_format: รูปà¹à¸šà¸šà¸à¸²à¸£à¹à¸ªà¸”งชื่อผู้ใช้ + setting_activity_days_default: จำนวนวันที่à¹à¸ªà¸”งในà¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸‚องโครงà¸à¸²à¸£ + setting_display_subprojects_issues: à¹à¸ªà¸”งปัà¸à¸«à¸²à¸‚องโครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢à¹ƒà¸™à¹‚ครงà¸à¸²à¸£à¸«à¸¥à¸±à¸ + + 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: à¸à¸£à¸°à¸”านข้อความ + + label_user: ผู้ใช้ + label_user_plural: ผู้ใช้ + label_user_new: ผู้ใช้ใหม่ + label_project: โครงà¸à¸²à¸£ + label_project_new: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆ + label_project_plural: โครงà¸à¸²à¸£ + label_x_projects: + zero: no projects + one: 1 project + other: "%{count} projects" + 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_password_lost: ลืมรหัสผ่าน + label_home: หน้าà¹à¸£à¸ + label_my_page: หน้าของฉัน + label_my_account: บัà¸à¸Šà¸µà¸‚องฉัน + label_my_projects: โครงà¸à¸²à¸£à¸‚องฉัน + 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_activity_plural: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ + label_activity_latest: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸¥à¹ˆà¸²à¸ªà¸¸à¸” + label_overall_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¹‚ดยรวม + label_new: ใหม่ + label_logged_as: เข้าระบบในชื่อ + label_environment: สภาพà¹à¸§à¸”ล้อม + label_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน + label_auth_source: วิธีà¸à¸²à¸£à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน + label_auth_source_new: สร้างวิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนใหม่ + label_auth_source_plural: วิธีà¸à¸²à¸£ Authentication + label_subproject_plural: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢ + 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_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 open / %{total} + one: 1 open / %{total} + other: "%{count} open / %{total}" + label_x_open_issues_abbr: + zero: 0 open + one: 1 open + other: "%{count} open" + label_x_closed_issues_abbr: + zero: 0 closed + one: 1 closed + other: "%{count} closed" + 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: Gantt + label_internal: ภายใน + label_last_changes: "last %{count} เปลี่ยนà¹à¸›à¸¥à¸‡" + label_change_view_all: ดูà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด + label_personalize_page: ปรับà¹à¸•่งหน้านี้ + label_comment: ความเห็น + label_comment_plural: ความเห็น + label_x_comments: + zero: no comments + one: 1 comment + other: "%{count} comments" + 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_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_revision: à¸à¸²à¸£à¹à¸à¹‰à¹„ข + label_revision_plural: à¸à¸²à¸£à¹à¸à¹‰à¹„ข + label_associated_revisions: à¸à¸²à¸£à¹à¸à¹‰à¹„ขที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง + label_added: ถูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_modified: ถูà¸à¹à¸à¹‰à¹„ข + label_deleted: ถูà¸à¸¥à¸š + label_latest_revision: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด + label_latest_revision_plural: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด + label_view_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_f_hour: "%{value} ชั่วโมง" + label_f_hour_plural: "%{value} ชั่วโมง" + label_time_tracking: ติดตามà¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸² + label_change_plural: เปลี่ยนà¹à¸›à¸¥à¸‡ + label_statistics: สถิติ + label_commits_per_month: Commits ต่อเดือน + label_commits_per_author: Commits ต่อผู้à¹à¸•่ง + label_view_diff: ดูความà¹à¸•à¸à¸•่าง + label_diff_inline: inline + label_diff_side_by_side: 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_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_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_created_on: "Atom access key สร้างเมื่อ %{value} ที่ผ่านมา" + label_module_plural: ส่วนประà¸à¸­à¸š + label_added_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_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: ตัวจัดà¸à¸²à¸£à¸•้นฉบับ + 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: à¸à¸²à¸£à¸§à¸²à¸‡à¹à¸œà¸™ + + button_login: เข้าระบบ + button_submit: จัดส่งข้อมูล + button_save: จัดเà¸à¹‡à¸š + button_check_all: เลือà¸à¸—ั้งหมด + button_uncheck_all: ไม่เลือà¸à¸—ั้งหมด + button_delete: ลบ + button_create: สร้าง + button_test: ทดสอบ + button_edit: à¹à¸à¹‰à¹„ข + button_add: เพิ่ม + button_change: เปลี่ยนà¹à¸›à¸¥à¸‡ + button_apply: ประยุà¸à¸•์ใช้ + button_clear: ล้างข้อความ + button_lock: ล็อค + button_unlock: ยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸¥à¹‡à¸­à¸„ + button_download: ดาวน์โหลด + button_list: รายà¸à¸²à¸£ + button_view: มุมมอง + button_move: ย้าย + 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_annotate: หมายเหตุประà¸à¸­à¸š + button_update: ปรับปรุง + button_configure: ปรับà¹à¸•่ง + + status_active: เปิดใช้งานà¹à¸¥à¹‰à¸§ + status_registered: รอà¸à¸²à¸£à¸­à¸™à¸¸à¸¡à¸±à¸•ิ + status_locked: ล็อค + + 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_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_issues_ref_in_commit_messages: Referencing and fixing issues 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_issues_destroy_confirmation: 'คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้องà¸à¸²à¸£à¸¥à¸šà¸›à¸±à¸à¸«à¸²(ทั้งหลาย)ที่เลือà¸à¹„ว้?' + text_select_project_modules: 'เลือà¸à¸ªà¹ˆà¸§à¸™à¸›à¸£à¸°à¸à¸­à¸šà¸—ี่ต้องà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸ªà¸³à¸«à¸£à¸±à¸šà¹‚ครงà¸à¸²à¸£à¸™à¸µà¹‰:' + text_default_administrator_account_changed: ค่าเริ่มต้นของบัà¸à¸Šà¸µà¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸–ูà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ + text_file_repository_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: 'ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹ˆà¸­à¸µà¸à¸„รั้ง:' + + 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: 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: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ (ใช้ในà¸à¸²à¸£à¸•ิดตามเวลา) + label_and_its_subprojects: "%{value} and its subprojects" + mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:" + mail_subject_reminder: "%{count} issue(s) due in the next %{days} days" + text_user_wrote: "%{value} wrote:" + label_duplicated_by: duplicated by + setting_enabled_scm: Enabled SCM + text_enumeration_category_reassign_to: 'Reassign them to this value:' + text_enumeration_destroy_question: "%{count} objects are assigned to this value." + label_incoming_emails: Incoming emails + label_generate_key: Generate a key + setting_mail_handler_api_enabled: Enable WS for incoming emails + setting_mail_handler_api_key: API key + text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." + field_parent_title: Parent page + label_issue_watchers: Watchers + button_quote: Quote + setting_sequential_project_identifiers: Generate sequential project identifiers + notice_unable_delete_version: Unable to delete version + label_renamed: renamed + label_copied: copied + setting_plain_text_mail: plain text only (no HTML) + permission_view_files: View files + permission_edit_issues: Edit issues + permission_edit_own_time_entries: Edit own time logs + permission_manage_public_queries: Manage public queries + permission_add_issues: Add issues + permission_log_time: Log spent time + permission_view_changesets: View changesets + permission_view_time_entries: View spent time + permission_manage_versions: Manage versions + permission_manage_wiki: Manage wiki + permission_manage_categories: Manage issue categories + permission_protect_wiki_pages: Protect wiki pages + permission_comment_news: Comment news + permission_delete_messages: Delete messages + permission_select_project_modules: Select project modules + permission_edit_wiki_pages: Edit wiki pages + permission_add_issue_watchers: Add watchers + permission_view_gantt: View gantt chart + permission_move_issues: Move issues + permission_manage_issue_relations: Manage issue relations + permission_delete_wiki_pages: Delete wiki pages + permission_manage_boards: Manage boards + permission_delete_wiki_pages_attachments: Delete attachments + permission_view_wiki_edits: View wiki history + permission_add_messages: Post messages + permission_view_messages: View messages + permission_manage_files: Manage files + permission_edit_issue_notes: Edit notes + permission_manage_news: Manage news + permission_view_calendar: View calendrier + permission_manage_members: Manage members + permission_edit_messages: Edit messages + permission_delete_issues: Delete issues + permission_view_issue_watchers: View watchers list + permission_manage_repository: Manage repository + permission_commit_access: Commit access + permission_browse_repository: Browse repository + permission_view_documents: View documents + permission_edit_project: Edit project + permission_add_issue_notes: Add notes + permission_save_queries: Save queries + permission_view_wiki_pages: View wiki + permission_rename_wiki_pages: Rename wiki pages + permission_edit_time_entries: Edit time logs + permission_edit_own_issue_notes: Edit own notes + setting_gravatar_enabled: Use Gravatar user icons + label_example: Example + text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." + permission_edit_own_messages: Edit own messages + permission_delete_own_messages: Delete own messages + label_user_activity: "%{value}'s activity" + label_updated_time_by: "Updated by %{author} %{age} ago" + text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' + setting_diff_max_lines_displayed: Max number of diff lines displayed + text_plugin_assets_writable: Plugin assets directory writable + warning_attachments_not_saved: "%{count} file(s) could not be saved." + button_create_and_continue: Create and continue + text_custom_field_possible_values_info: 'One line for each value' + label_display: Display + field_editable: Editable + setting_repository_log_display_limit: Maximum number of revisions displayed on file log + setting_file_max_size_displayed: Max size of text files displayed inline + field_watcher: Watcher + setting_openid: Allow OpenID login and registration + field_identity_url: OpenID URL + label_login_with_open_id_option: or login with OpenID + field_content: Content + label_descending: Descending + label_sort: Sort + label_ascending: Ascending + label_date_from_to: From %{start} to %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: This page has %{descendants} child page(s) and descendant(s). What do you want to do? + text_wiki_page_reassign_children: Reassign child pages to this parent page + text_wiki_page_nullify_children: Keep child pages as root pages + text_wiki_page_destroy_children: Delete child pages and all their descendants + setting_password_min_length: Minimum password length + field_group_by: Group results by + mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" + label_wiki_content_added: Wiki page added + mail_subject_wiki_content_added: "'%{id}' wiki page has been added" + mail_body_wiki_content_added: The '%{id}' wiki page has been added by %{author}. + label_wiki_content_updated: Wiki page updated + mail_body_wiki_content_updated: The '%{id}' wiki page has been updated by %{author}. + permission_add_project: Create project + setting_new_project_user_role_id: Role given to a non-admin user who creates a project + label_view_all_revisions: View all revisions + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: No tracker is associated to this project. Please check the Project settings. + error_no_default_issue_status: No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses"). + text_journal_changed: "%{label} changed from %{old} to %{new}" + text_journal_set_to: "%{label} set to %{value}" + text_journal_deleted: "%{label} deleted (%{old})" + label_group_plural: Groups + label_group: Group + label_group_new: New group + label_time_entry_plural: Spent time + text_journal_added: "%{label} %{value} added" + field_active: Active + enumeration_system_activity: System Activity + permission_delete_issue_watchers: Delete watchers + version_status_closed: closed + version_status_locked: locked + version_status_open: open + error_can_not_reopen_issue_on_closed_version: An issue assigned to a closed version can not be reopened + label_user_anonymous: Anonymous + button_move_and_follow: Move and follow + setting_default_projects_modules: Default enabled modules for new projects + setting_gravatar_default: Default Gravatar image + field_sharing: Sharing + label_version_sharing_hierarchy: With project hierarchy + label_version_sharing_system: With all projects + label_version_sharing_descendants: With subprojects + label_version_sharing_tree: With project tree + label_version_sharing_none: Not shared + error_can_not_archive_project: This project can not be archived + button_duplicate: Duplicate + button_copy_and_follow: Copy and follow + label_copy_source: Source + setting_issue_done_ratio: Calculate the issue done ratio with + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + error_workflow_copy_target: Please select target tracker(s) and role(s) + setting_issue_done_ratio_issue_field: Use the issue field + label_copy_same_as_target: Same as target + label_copy_target: Target + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + label_update_issue_done_ratios: Update issue done ratios + setting_start_of_week: Start calendars on + permission_view_issues: View Issues + label_display_used_statuses_only: Only display statuses that are used by this tracker + label_revision_id: Revision %{value} + label_api_access_key: API access key + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: Atom access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_missing_api_access_key: Missing an API access key + label_missing_feeds_access_key: Missing a Atom access key + button_show: Show + text_line_separated: Multiple values allowed (one line for each value). + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: Commit messages 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: ทั้งหมด + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: With subprojects + label_cross_project_tree: With project tree + label_cross_project_hierarchy: With project hierarchy + label_cross_project_system: With all projects + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: จำนวนรวม + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/08415c997f1d03456d84b86652b037399f12d911.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08415c997f1d03456d84b86652b037399f12d911.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,119 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 LayoutTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + test "browsing to a missing page should render the base layout" do + get "/users/100000000" + + assert_response :not_found + + # UsersController uses the admin layout by default + assert_select "#admin-menu", :count => 0 + end + + test "browsing to an unauthorized page should render the base layout" do + change_user_password('miscuser9', 'test1234') + + log_user('miscuser9','test1234') + + get "/admin" + assert_response :forbidden + assert_select "#admin-menu", :count => 0 + end + + def test_top_menu_and_search_not_visible_when_login_required + with_settings :login_required => '1' do + get '/' + assert_select "#top-menu > ul", 0 + assert_select "#quick-search", 0 + end + end + + def test_top_menu_and_search_visible_when_login_not_required + with_settings :login_required => '0' do + get '/' + assert_select "#top-menu > ul" + assert_select "#quick-search" + end + end + + def test_wiki_formatter_header_tags + Role.anonymous.add_permission! :add_issues + + get '/projects/ecookbook/issues/new' + assert_tag :script, + :attributes => {:src => %r{^/javascripts/jstoolbar/jstoolbar-textile.min.js}}, + :parent => {:tag => 'head'} + end + + def test_calendar_header_tags + with_settings :default_language => 'fr' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-fr.js", response.body + end + + with_settings :default_language => 'en-GB' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-en-GB.js", response.body + end + + with_settings :default_language => 'en' do + get '/issues' + assert_not_include "/javascripts/i18n/jquery.ui.datepicker", response.body + end + + with_settings :default_language => 'zh' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-zh-CN.js", response.body + end + + with_settings :default_language => 'zh-TW' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-zh-TW.js", response.body + end + + with_settings :default_language => 'pt' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-pt.js", response.body + end + + with_settings :default_language => 'pt-BR' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-pt-BR.js", response.body + end + end + + def test_search_field_outside_project_should_link_to_global_search + get '/' + assert_select 'div#quick-search form[action=/search]' + end + + def test_search_field_inside_project_should_link_to_project_search + get '/projects/ecookbook' + assert_select 'div#quick-search form[action=/projects/ecookbook/search]' + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/084dba3511eace99e8953d1d7626d2fa107595d7.svn-base --- a/.svn/pristine/08/084dba3511eace99e8953d1d7626d2fa107595d7.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 NewsController < ApplicationController - default_search_scope :news - model_object News - before_filter :find_model_object, :except => [:new, :create, :index] - before_filter :find_project_from_association, :except => [:new, :create, :index] - before_filter :find_project_by_project_id, :only => [:new, :create] - before_filter :authorize, :except => [:index] - before_filter :find_optional_project, :only => :index - accept_rss_auth :index - accept_api_auth :index - - helper :watchers - helper :attachments - - def index - case params[:format] - when 'xml', 'json' - @offset, @limit = api_offset_and_limit - else - @limit = 10 - end - - scope = @project ? @project.news.visible : News.visible - - @news_count = scope.count - @news_pages = Paginator.new self, @news_count, @limit, params['page'] - @offset ||= @news_pages.current.offset - @newss = scope.all(:include => [:author, :project], - :order => "#{News.table_name}.created_on DESC", - :offset => @offset, - :limit => @limit) - - respond_to do |format| - format.html { - @news = News.new # for adding news inline - render :layout => false if request.xhr? - } - format.api - format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } - end - end - - def show - @comments = @news.comments - @comments.reverse! if User.current.wants_comments_in_reverse_order? - end - - def new - @news = News.new(:project => @project, :author => User.current) - end - - def create - @news = News.new(:project => @project, :author => User.current) - @news.safe_attributes = params[:news] - @news.save_attachments(params[:attachments]) - if @news.save - render_attachment_warning_if_needed(@news) - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'news', :action => 'index', :project_id => @project - else - render :action => 'new' - end - end - - def edit - end - - def update - @news.safe_attributes = params[:news] - @news.save_attachments(params[:attachments]) - if @news.save - render_attachment_warning_if_needed(@news) - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'show', :id => @news - else - render :action => 'edit' - end - end - - def destroy - @news.destroy - redirect_to :action => 'index', :project_id => @project - end - - private - - def find_optional_project - return true unless params[:project_id] - @project = Project.find(params[:project_id]) - authorize - rescue ActiveRecord::RecordNotFound - render_404 - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/0857a2b98bb983778cfcdfd0cbe755f5b48aa4d8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/0857a2b98bb983778cfcdfd0cbe755f5b48aa4d8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,947 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 ProjectTest < ActiveSupport::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :journals, :journal_details, + :enumerations, :users, :issue_categories, + :projects_trackers, + :custom_fields, + :custom_fields_projects, + :custom_fields_trackers, + :custom_values, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, + :groups_users, + :boards, :messages, + :repositories, + :news, :comments, + :documents + + def setup + @ecookbook = Project.find(1) + @ecookbook_sub1 = Project.find(3) + set_tmp_attachments_directory + User.current = nil + end + + def test_truth + assert_kind_of Project, @ecookbook + assert_equal "eCookbook", @ecookbook.name + end + + def test_default_attributes + with_settings :default_projects_public => '1' do + assert_equal true, Project.new.is_public + assert_equal false, Project.new(:is_public => false).is_public + end + + with_settings :default_projects_public => '0' do + assert_equal false, Project.new.is_public + assert_equal true, Project.new(:is_public => true).is_public + end + + with_settings :sequential_project_identifiers => '1' do + assert !Project.new.identifier.blank? + assert Project.new(:identifier => '').identifier.blank? + end + + with_settings :sequential_project_identifiers => '0' do + assert Project.new.identifier.blank? + assert !Project.new(:identifier => 'test').blank? + end + + with_settings :default_projects_modules => ['issue_tracking', 'repository'] do + assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names + end + end + + def test_default_trackers_should_match_default_tracker_ids_setting + with_settings :default_projects_tracker_ids => ['1', '3'] do + assert_equal Tracker.find(1, 3).sort, Project.new.trackers.sort + end + end + + def test_default_trackers_should_be_all_trackers_with_blank_setting + with_settings :default_projects_tracker_ids => nil do + assert_equal Tracker.all.sort, Project.new.trackers.sort + end + end + + def test_default_trackers_should_be_empty_with_empty_setting + with_settings :default_projects_tracker_ids => [] do + assert_equal [], Project.new.trackers + end + end + + def test_default_trackers_should_not_replace_initialized_trackers + with_settings :default_projects_tracker_ids => ['1', '3'] do + assert_equal Tracker.find(1, 2).sort, Project.new(:tracker_ids => [1, 2]).trackers.sort + end + end + + def test_update + assert_equal "eCookbook", @ecookbook.name + @ecookbook.name = "eCook" + assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ") + @ecookbook.reload + assert_equal "eCook", @ecookbook.name + end + + def test_validate_identifier + to_test = {"abc" => true, + "ab12" => true, + "ab-12" => true, + "ab_12" => true, + "12" => false, + "new" => false} + + to_test.each do |identifier, valid| + p = Project.new + p.identifier = identifier + p.valid? + if valid + assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid" + else + assert p.errors['identifier'].present?, "identifier #{identifier} was valid" + end + end + end + + def test_identifier_should_not_be_frozen_for_a_new_project + assert_equal false, Project.new.identifier_frozen? + end + + def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier + Project.where(:id => 1).update_all(["identifier = ''"]) + assert_equal false, Project.find(1).identifier_frozen? + end + + def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier + assert_equal true, Project.find(1).identifier_frozen? + end + + def test_members_should_be_active_users + Project.all.each do |project| + assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) } + end + end + + def test_users_should_be_active_users + Project.all.each do |project| + assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) } + end + end + + def test_open_scope_on_issues_association + assert_kind_of Issue, Project.find(1).issues.open.first + end + + def test_archive + user = @ecookbook.members.first.user + @ecookbook.archive + @ecookbook.reload + + assert !@ecookbook.active? + assert @ecookbook.archived? + assert !user.projects.include?(@ecookbook) + # Subproject are also archived + assert !@ecookbook.children.empty? + assert @ecookbook.descendants.active.empty? + end + + def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects + # Assign an issue of a project to a version of a child project + Issue.find(4).update_attribute :fixed_version_id, 4 + + assert_no_difference "Project.where(:status => Project::STATUS_ARCHIVED).count" do + assert_equal false, @ecookbook.archive + end + @ecookbook.reload + assert @ecookbook.active? + end + + def test_unarchive + user = @ecookbook.members.first.user + @ecookbook.archive + # A subproject of an archived project can not be unarchived + assert !@ecookbook_sub1.unarchive + + # Unarchive project + assert @ecookbook.unarchive + @ecookbook.reload + assert @ecookbook.active? + assert !@ecookbook.archived? + assert user.projects.include?(@ecookbook) + # Subproject can now be unarchived + @ecookbook_sub1.reload + assert @ecookbook_sub1.unarchive + end + + def test_destroy + # 2 active members + assert_equal 2, @ecookbook.members.size + # and 1 is locked + assert_equal 3, Member.where(:project_id => @ecookbook.id).count + # some boards + assert @ecookbook.boards.any? + + @ecookbook.destroy + # make sure that the project non longer exists + assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) } + # make sure related data was removed + assert_nil Member.where(:project_id => @ecookbook.id).first + assert_nil Board.where(:project_id => @ecookbook.id).first + assert_nil Issue.where(:project_id => @ecookbook.id).first + end + + def test_destroy_should_destroy_subtasks + issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')} + issues[0].update_attribute :parent_issue_id, issues[1].id + issues[2].update_attribute :parent_issue_id, issues[1].id + assert_equal 2, issues[1].children.count + + assert_nothing_raised do + Project.find(1).destroy + end + assert_equal 0, Issue.where(:id => issues.map(&:id)).count + end + + def test_destroying_root_projects_should_clear_data + Project.roots.each do |root| + root.destroy + end + + assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}" + assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}" + assert_equal 0, MemberRole.count + assert_equal 0, Issue.count + assert_equal 0, Journal.count + assert_equal 0, JournalDetail.count + assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}" + assert_equal 0, EnabledModule.count + assert_equal 0, IssueCategory.count + assert_equal 0, IssueRelation.count + assert_equal 0, Board.count + assert_equal 0, Message.count + assert_equal 0, News.count + assert_equal 0, Query.where("project_id IS NOT NULL").count + assert_equal 0, Repository.count + assert_equal 0, Changeset.count + assert_equal 0, Change.count + assert_equal 0, Comment.count + assert_equal 0, TimeEntry.count + assert_equal 0, Version.count + assert_equal 0, Watcher.count + assert_equal 0, Wiki.count + assert_equal 0, WikiPage.count + assert_equal 0, WikiContent.count + assert_equal 0, WikiContent::Version.count + assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").count + assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").count + assert_equal 0, CustomValue.where(:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']).count + end + + def test_destroy_should_delete_time_entries_custom_values + project = Project.generate! + time_entry = TimeEntry.generate!(:project => project, :custom_field_values => {10 => '1'}) + + assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do + assert project.destroy + end + end + + def test_move_an_orphan_project_to_a_root_project + sub = Project.find(2) + sub.set_parent! @ecookbook + assert_equal @ecookbook.id, sub.parent.id + @ecookbook.reload + assert_equal 4, @ecookbook.children.size + end + + def test_move_an_orphan_project_to_a_subproject + sub = Project.find(2) + assert sub.set_parent!(@ecookbook_sub1) + end + + def test_move_a_root_project_to_a_project + sub = @ecookbook + assert sub.set_parent!(Project.find(2)) + end + + def test_should_not_move_a_project_to_its_children + sub = @ecookbook + assert !(sub.set_parent!(Project.find(3))) + end + + def test_set_parent_should_add_roots_in_alphabetical_order + ProjectCustomField.delete_all + Project.delete_all + Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil) + Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil) + Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil) + Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil) + + assert_equal 4, Project.count + assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft) + end + + def test_set_parent_should_add_children_in_alphabetical_order + ProjectCustomField.delete_all + parent = Project.create!(:name => 'Parent', :identifier => 'parent') + Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent) + Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent) + Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent) + Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent) + + parent.reload + assert_equal 4, parent.children.size + assert_equal parent.children.all.sort_by(&:name), parent.children.all + end + + def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy + # Parent issue with a hierarchy project's fixed version + parent_issue = Issue.find(1) + parent_issue.update_attribute(:fixed_version_id, 4) + parent_issue.reload + assert_equal 4, parent_issue.fixed_version_id + + # Should keep fixed versions for the issues + issue_with_local_fixed_version = Issue.find(5) + issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4) + issue_with_local_fixed_version.reload + assert_equal 4, issue_with_local_fixed_version.fixed_version_id + + # Local issue with hierarchy fixed_version + issue_with_hierarchy_fixed_version = Issue.find(13) + issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6) + issue_with_hierarchy_fixed_version.reload + assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id + + # Move project out of the issue's hierarchy + moved_project = Project.find(3) + moved_project.set_parent!(Project.find(2)) + parent_issue.reload + issue_with_local_fixed_version.reload + issue_with_hierarchy_fixed_version.reload + + assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project" + assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in" + assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue." + end + + def test_parent + p = Project.find(6).parent + assert p.is_a?(Project) + assert_equal 5, p.id + end + + def test_ancestors + a = Project.find(6).ancestors + assert a.first.is_a?(Project) + assert_equal [1, 5], a.collect(&:id) + end + + def test_root + r = Project.find(6).root + assert r.is_a?(Project) + assert_equal 1, r.id + end + + def test_children + c = Project.find(1).children + assert c.first.is_a?(Project) + assert_equal [5, 3, 4], c.collect(&:id) + end + + def test_descendants + d = Project.find(1).descendants + assert d.first.is_a?(Project) + assert_equal [5, 6, 3, 4], d.collect(&:id) + end + + def test_allowed_parents_should_be_empty_for_non_member_user + Role.non_member.add_permission!(:add_project) + user = User.find(9) + assert user.memberships.empty? + User.current = user + assert Project.new.allowed_parents.compact.empty? + end + + def test_allowed_parents_with_add_subprojects_permission + Role.find(1).remove_permission!(:add_project) + Role.find(1).add_permission!(:add_subprojects) + User.current = User.find(2) + # new project + assert !Project.new.allowed_parents.include?(nil) + assert Project.new.allowed_parents.include?(Project.find(1)) + # existing root project + assert Project.find(1).allowed_parents.include?(nil) + # existing child + assert Project.find(3).allowed_parents.include?(Project.find(1)) + assert !Project.find(3).allowed_parents.include?(nil) + end + + def test_allowed_parents_with_add_project_permission + Role.find(1).add_permission!(:add_project) + Role.find(1).remove_permission!(:add_subprojects) + User.current = User.find(2) + # new project + assert Project.new.allowed_parents.include?(nil) + assert !Project.new.allowed_parents.include?(Project.find(1)) + # existing root project + assert Project.find(1).allowed_parents.include?(nil) + # existing child + assert Project.find(3).allowed_parents.include?(Project.find(1)) + assert Project.find(3).allowed_parents.include?(nil) + end + + def test_allowed_parents_with_add_project_and_subprojects_permission + Role.find(1).add_permission!(:add_project) + Role.find(1).add_permission!(:add_subprojects) + User.current = User.find(2) + # new project + assert Project.new.allowed_parents.include?(nil) + assert Project.new.allowed_parents.include?(Project.find(1)) + # existing root project + assert Project.find(1).allowed_parents.include?(nil) + # existing child + assert Project.find(3).allowed_parents.include?(Project.find(1)) + assert Project.find(3).allowed_parents.include?(nil) + end + + def test_users_by_role + users_by_role = Project.find(1).users_by_role + assert_kind_of Hash, users_by_role + role = Role.find(1) + assert_kind_of Array, users_by_role[role] + assert users_by_role[role].include?(User.find(2)) + end + + def test_rolled_up_trackers + parent = Project.find(1) + parent.trackers = Tracker.find([1,2]) + child = parent.children.find(3) + + assert_equal [1, 2], parent.tracker_ids + assert_equal [2, 3], child.trackers.collect(&:id) + + assert_kind_of Tracker, parent.rolled_up_trackers.first + assert_equal Tracker.find(1), parent.rolled_up_trackers.first + + assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id) + assert_equal [2, 3], child.rolled_up_trackers.collect(&:id) + end + + def test_rolled_up_trackers_should_ignore_archived_subprojects + parent = Project.find(1) + parent.trackers = Tracker.find([1,2]) + child = parent.children.find(3) + child.trackers = Tracker.find([1,3]) + parent.children.each(&:archive) + + assert_equal [1,2], parent.rolled_up_trackers.collect(&:id) + end + + test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do + parent = Project.generate! + parent.trackers = Tracker.find([1, 2]) + child = Project.generate_with_parent!(parent) + child.trackers = Tracker.find([2, 3]) + + assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort + + assert child.disable_module!(:issue_tracking) + parent.reload + assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort + end + + test "#rolled_up_versions should include the versions for the current project" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions + end + + test "#rolled_up_versions should include versions for a subproject" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + subproject = Project.generate_with_parent!(project) + subproject_version = Version.generate!(:project => subproject) + + assert_same_elements [ + parent_version_1, + parent_version_2, + subproject_version + ], project.rolled_up_versions + end + + test "#rolled_up_versions should include versions for a sub-subproject" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + subproject = Project.generate_with_parent!(project) + sub_subproject = Project.generate_with_parent!(subproject) + sub_subproject_version = Version.generate!(:project => sub_subproject) + project.reload + + assert_same_elements [ + parent_version_1, + parent_version_2, + sub_subproject_version + ], project.rolled_up_versions + end + + test "#rolled_up_versions should only check active projects" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + subproject = Project.generate_with_parent!(project) + subproject_version = Version.generate!(:project => subproject) + assert subproject.archive + project.reload + + assert !subproject.active? + assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions + end + + def test_shared_versions_none_sharing + p = Project.find(5) + v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none') + assert p.shared_versions.include?(v) + assert !p.children.first.shared_versions.include?(v) + assert !p.root.shared_versions.include?(v) + assert !p.siblings.first.shared_versions.include?(v) + assert !p.root.siblings.first.shared_versions.include?(v) + end + + def test_shared_versions_descendants_sharing + p = Project.find(5) + v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants') + assert p.shared_versions.include?(v) + assert p.children.first.shared_versions.include?(v) + assert !p.root.shared_versions.include?(v) + assert !p.siblings.first.shared_versions.include?(v) + assert !p.root.siblings.first.shared_versions.include?(v) + end + + def test_shared_versions_hierarchy_sharing + p = Project.find(5) + v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy') + assert p.shared_versions.include?(v) + assert p.children.first.shared_versions.include?(v) + assert p.root.shared_versions.include?(v) + assert !p.siblings.first.shared_versions.include?(v) + assert !p.root.siblings.first.shared_versions.include?(v) + end + + def test_shared_versions_tree_sharing + p = Project.find(5) + v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree') + assert p.shared_versions.include?(v) + assert p.children.first.shared_versions.include?(v) + assert p.root.shared_versions.include?(v) + assert p.siblings.first.shared_versions.include?(v) + assert !p.root.siblings.first.shared_versions.include?(v) + end + + def test_shared_versions_system_sharing + p = Project.find(5) + v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system') + assert p.shared_versions.include?(v) + assert p.children.first.shared_versions.include?(v) + assert p.root.shared_versions.include?(v) + assert p.siblings.first.shared_versions.include?(v) + assert p.root.siblings.first.shared_versions.include?(v) + end + + def test_shared_versions + parent = Project.find(1) + child = parent.children.find(3) + private_child = parent.children.find(5) + + assert_equal [1,2,3], parent.version_ids.sort + assert_equal [4], child.version_ids + assert_equal [6], private_child.version_ids + assert_equal [7], Version.where(:sharing => 'system').collect(&:id) + + assert_equal 6, parent.shared_versions.size + parent.shared_versions.each do |version| + assert_kind_of Version, version + end + + assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort + end + + def test_shared_versions_should_ignore_archived_subprojects + parent = Project.find(1) + child = parent.children.find(3) + child.archive + parent.reload + + assert_equal [1,2,3], parent.version_ids.sort + assert_equal [4], child.version_ids + assert !parent.shared_versions.collect(&:id).include?(4) + end + + def test_shared_versions_visible_to_user + user = User.find(3) + parent = Project.find(1) + child = parent.children.find(5) + + assert_equal [1,2,3], parent.version_ids.sort + assert_equal [6], child.version_ids + + versions = parent.shared_versions.visible(user) + + assert_equal 4, versions.size + versions.each do |version| + assert_kind_of Version, version + end + + assert !versions.collect(&:id).include?(6) + end + + def test_shared_versions_for_new_project_should_include_system_shared_versions + p = Project.find(5) + v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system') + + assert_include v, Project.new.shared_versions + end + + def test_next_identifier + ProjectCustomField.delete_all + Project.create!(:name => 'last', :identifier => 'p2008040') + assert_equal 'p2008041', Project.next_identifier + end + + def test_next_identifier_first_project + Project.delete_all + assert_nil Project.next_identifier + end + + def test_enabled_module_names + with_settings :default_projects_modules => ['issue_tracking', 'repository'] do + project = Project.new + + project.enabled_module_names = %w(issue_tracking news) + assert_equal %w(issue_tracking news), project.enabled_module_names.sort + end + end + + test "enabled_modules should define module by names and preserve ids" do + @project = Project.find(1) + # Remove one module + modules = @project.enabled_modules.slice(0..-2) + assert modules.any? + assert_difference 'EnabledModule.count', -1 do + @project.enabled_module_names = modules.collect(&:name) + end + @project.reload + # Ids should be preserved + assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort + end + + test "enabled_modules should enable a module" do + @project = Project.find(1) + @project.enabled_module_names = [] + @project.reload + assert_equal [], @project.enabled_module_names + #with string + @project.enable_module!("issue_tracking") + assert_equal ["issue_tracking"], @project.enabled_module_names + #with symbol + @project.enable_module!(:gantt) + assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names + #don't add a module twice + @project.enable_module!("issue_tracking") + assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names + end + + test "enabled_modules should disable a module" do + @project = Project.find(1) + #with string + assert @project.enabled_module_names.include?("issue_tracking") + @project.disable_module!("issue_tracking") + assert ! @project.reload.enabled_module_names.include?("issue_tracking") + #with symbol + assert @project.enabled_module_names.include?("gantt") + @project.disable_module!(:gantt) + assert ! @project.reload.enabled_module_names.include?("gantt") + #with EnabledModule object + first_module = @project.enabled_modules.first + @project.disable_module!(first_module) + assert ! @project.reload.enabled_module_names.include?(first_module.name) + end + + def test_enabled_module_names_should_not_recreate_enabled_modules + project = Project.find(1) + # Remove one module + modules = project.enabled_modules.slice(0..-2) + assert modules.any? + assert_difference 'EnabledModule.count', -1 do + project.enabled_module_names = modules.collect(&:name) + end + project.reload + # Ids should be preserved + assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort + end + + def test_copy_from_existing_project + source_project = Project.find(1) + copied_project = Project.copy_from(1) + + assert copied_project + # Cleared attributes + assert copied_project.id.blank? + assert copied_project.name.blank? + assert copied_project.identifier.blank? + + # Duplicated attributes + assert_equal source_project.description, copied_project.description + assert_equal source_project.enabled_modules, copied_project.enabled_modules + assert_equal source_project.trackers, copied_project.trackers + + # Default attributes + assert_equal 1, copied_project.status + end + + def test_activities_should_use_the_system_activities + project = Project.find(1) + assert_equal project.activities, TimeEntryActivity.where(:active => true).all + assert_kind_of ActiveRecord::Relation, project.activities + end + + + def test_activities_should_use_the_project_specific_activities + project = Project.find(1) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project}) + assert overridden_activity.save! + + assert project.activities.include?(overridden_activity), "Project specific Activity not found" + assert_kind_of ActiveRecord::Relation, project.activities + end + + def test_activities_should_not_include_the_inactive_project_specific_activities + project = Project.find(1) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false}) + assert overridden_activity.save! + + assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found" + end + + def test_activities_should_not_include_project_specific_activities_from_other_projects + project = Project.find(1) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)}) + assert overridden_activity.save! + + assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project" + end + + def test_activities_should_handle_nils + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first}) + TimeEntryActivity.delete_all + + # No activities + project = Project.find(1) + assert project.activities.empty? + + # No system, one overridden + assert overridden_activity.save! + project.reload + assert_equal [overridden_activity], project.activities + end + + def test_activities_should_override_system_activities_with_project_activities + project = Project.find(1) + parent_activity = TimeEntryActivity.first + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity}) + assert overridden_activity.save! + + assert project.activities.include?(overridden_activity), "Project specific Activity not found" + assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden" + end + + def test_activities_should_include_inactive_activities_if_specified + project = Project.find(1) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false}) + assert overridden_activity.save! + + assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found" + end + + test 'activities should not include active System activities if the project has an override that is inactive' do + project = Project.find(1) + system_activity = TimeEntryActivity.find_by_name('Design') + assert system_activity.active? + overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false) + assert overridden_activity.save! + + assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found" + assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override" + end + + def test_close_completed_versions + Version.update_all("status = 'open'") + project = Project.find(1) + assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'} + assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'} + project.close_completed_versions + project.reload + assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'} + assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'} + end + + test "#start_date should be nil if there are no issues on the project" do + project = Project.generate! + assert_nil project.start_date + end + + test "#start_date should be nil when issues have no start date" do + project = Project.generate! + project.trackers << Tracker.generate! + early = 7.days.ago.to_date + Issue.generate!(:project => project, :start_date => nil) + + assert_nil project.start_date + end + + test "#start_date should be the earliest start date of it's issues" do + project = Project.generate! + project.trackers << Tracker.generate! + early = 7.days.ago.to_date + Issue.generate!(:project => project, :start_date => Date.today) + Issue.generate!(:project => project, :start_date => early) + + assert_equal early, project.start_date + end + + test "#due_date should be nil if there are no issues on the project" do + project = Project.generate! + assert_nil project.due_date + end + + test "#due_date should be nil if there are no issues with due dates" do + project = Project.generate! + project.trackers << Tracker.generate! + Issue.generate!(:project => project, :due_date => nil) + + assert_nil project.due_date + end + + test "#due_date should be the latest due date of it's issues" do + project = Project.generate! + project.trackers << Tracker.generate! + future = 7.days.from_now.to_date + Issue.generate!(:project => project, :due_date => future) + Issue.generate!(:project => project, :due_date => Date.today) + + assert_equal future, project.due_date + end + + test "#due_date should be the latest due date of it's versions" do + project = Project.generate! + future = 7.days.from_now.to_date + project.versions << Version.generate!(:effective_date => future) + project.versions << Version.generate!(:effective_date => Date.today) + + assert_equal future, project.due_date + end + + test "#due_date should pick the latest date from it's issues and versions" do + project = Project.generate! + project.trackers << Tracker.generate! + future = 7.days.from_now.to_date + far_future = 14.days.from_now.to_date + Issue.generate!(:project => project, :due_date => far_future) + project.versions << Version.generate!(:effective_date => future) + + assert_equal far_future, project.due_date + end + + test "#completed_percent with no versions should be 100" do + project = Project.generate! + assert_equal 100, project.completed_percent + end + + test "#completed_percent with versions should return 0 if the versions have no issues" do + project = Project.generate! + Version.generate!(:project => project) + Version.generate!(:project => project) + + assert_equal 0, project.completed_percent + end + + test "#completed_percent with versions should return 100 if the version has only closed issues" do + project = Project.generate! + project.trackers << Tracker.generate! + v1 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1) + v2 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2) + + assert_equal 100, project.completed_percent + end + + test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do + project = Project.generate! + project.trackers << Tracker.generate! + v1 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1) + v2 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2) + + assert_equal 50, project.completed_percent + end + + test "#notified_users" do + project = Project.generate! + role = Role.generate! + + user_with_membership_notification = User.generate!(:mail_notification => 'selected') + Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true) + + all_events_user = User.generate!(:mail_notification => 'all') + Member.create!(:project => project, :roles => [role], :principal => all_events_user) + + no_events_user = User.generate!(:mail_notification => 'none') + Member.create!(:project => project, :roles => [role], :principal => no_events_user) + + only_my_events_user = User.generate!(:mail_notification => 'only_my_events') + Member.create!(:project => project, :roles => [role], :principal => only_my_events_user) + + only_assigned_user = User.generate!(:mail_notification => 'only_assigned') + Member.create!(:project => project, :roles => [role], :principal => only_assigned_user) + + only_owned_user = User.generate!(:mail_notification => 'only_owner') + Member.create!(:project => project, :roles => [role], :principal => only_owned_user) + + assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification" + assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option" + assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option" + assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option" + assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option" + assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option" + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/085f61120574ccfad3bedefd8a6623102e2bf32b.svn-base --- a/.svn/pristine/08/085f61120574ccfad3bedefd8a6623102e2bf32b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -<% content_for :header_tags do %> - <%= javascript_include_tag 'repository_navigation' %> -<% end %> - -<%= link_to l(:label_statistics), - {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param}, - :class => 'icon icon-stats' if @repository.supports_all_revisions? %> - -<%= form_tag({:action => controller.action_name, - :id => @project, - :repository_id => @repository.identifier_param, - :path => to_path_param(@path), - :rev => nil}, - {:method => :get, :id => 'revision_selector'}) do -%> - - <% if !@repository.branches.nil? && @repository.branches.length > 0 -%> - | <%= l(:label_branch) %>: - <%= select_tag :branch, - options_for_select([''] + @repository.branches, @rev), - :id => 'branch' %> - <% end -%> - - <% if !@repository.tags.nil? && @repository.tags.length > 0 -%> - | <%= l(:label_tag) %>: - <%= select_tag :tag, - options_for_select([''] + @repository.tags, @rev), - :id => 'tag' %> - <% end -%> - - <% if @repository.supports_all_revisions? %> - | <%= l(:label_revision) %>: - <%= text_field_tag 'rev', @rev, :size => 8 %> - <% end %> -<% end -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/08918fc8f853142db140298c79057a45064d5050.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08918fc8f853142db140298c79057a45064d5050.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .svn/pristine/08/08ad4f57c782749abd613605456ebd0acf953d61.svn-base --- a/.svn/pristine/08/08ad4f57c782749abd613605456ebd0acf953d61.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,978 +0,0 @@ -begin - require 'zlib' - @@__have_zlib = true -rescue - @@__have_zlib = false -end - -require 'rexml/document' - -module SVG - module Graph - VERSION = '@ANT_VERSION@' - - # === Base object for generating SVG Graphs - # - # == Synopsis - # - # This class is only used as a superclass of specialized charts. Do not - # attempt to use this class directly, unless creating a new chart type. - # - # For examples of how to subclass this class, see the existing specific - # subclasses, such as SVG::Graph::Pie. - # - # == Examples - # - # For examples of how to use this package, see either the test files, or - # the documentation for the specific class you want to use. - # - # * file:test/plot.rb - # * file:test/single.rb - # * file:test/test.rb - # * file:test/timeseries.rb - # - # == Description - # - # This package should be used as a base for creating SVG graphs. - # - # == Acknowledgements - # - # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby - # port is based on. - # - # Stephen Morgan for creating the TT template and SVG. - # - # == See - # - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Bar - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries - # - # == Author - # - # Sean E. Russell - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class Graph - include REXML - - # Initialize the graph object with the graph settings. You won't - # instantiate this class directly; see the subclass for options. - # [width] 500 - # [height] 300 - # [show_x_guidelines] false - # [show_y_guidelines] true - # [show_data_values] true - # [min_scale_value] 0 - # [show_x_labels] true - # [stagger_x_labels] false - # [rotate_x_labels] false - # [step_x_labels] 1 - # [step_include_first_x_label] true - # [show_y_labels] true - # [rotate_y_labels] false - # [scale_integers] false - # [show_x_title] false - # [x_title] 'X Field names' - # [show_y_title] false - # [y_title_text_direction] :bt - # [y_title] 'Y Scale' - # [show_graph_title] false - # [graph_title] 'Graph Title' - # [show_graph_subtitle] false - # [graph_subtitle] 'Graph Sub Title' - # [key] true, - # [key_position] :right, # bottom or righ - # [font_size] 12 - # [title_font_size] 16 - # [subtitle_font_size] 14 - # [x_label_font_size] 12 - # [x_title_font_size] 14 - # [y_label_font_size] 12 - # [y_title_font_size] 14 - # [key_font_size] 10 - # [no_css] false - # [add_popups] false - def initialize( config ) - @config = config - - self.top_align = self.top_font = self.right_align = self.right_font = 0 - - init_with({ - :width => 500, - :height => 300, - :show_x_guidelines => false, - :show_y_guidelines => true, - :show_data_values => true, - -# :min_scale_value => 0, - - :show_x_labels => true, - :stagger_x_labels => false, - :rotate_x_labels => false, - :step_x_labels => 1, - :step_include_first_x_label => true, - - :show_y_labels => true, - :rotate_y_labels => false, - :stagger_y_labels => false, - :scale_integers => false, - - :show_x_title => false, - :x_title => 'X Field names', - - :show_y_title => false, - :y_title_text_direction => :bt, - :y_title => 'Y Scale', - - :show_graph_title => false, - :graph_title => 'Graph Title', - :show_graph_subtitle => false, - :graph_subtitle => 'Graph Sub Title', - :key => true, - :key_position => :right, # bottom or right - - :font_size =>12, - :title_font_size =>16, - :subtitle_font_size =>14, - :x_label_font_size =>12, - :x_title_font_size =>14, - :y_label_font_size =>12, - :y_title_font_size =>14, - :key_font_size =>10, - - :no_css =>false, - :add_popups =>false, - }) - - set_defaults if respond_to? :set_defaults - - init_with config - end - - - # This method allows you do add data to the graph object. - # It can be called several times to add more data sets in. - # - # data_sales_02 = [12, 45, 21]; - # - # graph.add_data({ - # :data => data_sales_02, - # :title => 'Sales 2002' - # }) - def add_data conf - @data = [] unless defined? @data - - if conf[:data] and conf[:data].kind_of? Array - @data << conf - else - raise "No data provided by #{conf.inspect}" - end - end - - - # This method removes all data from the object so that you can - # reuse it to create a new graph but with the same config options. - # - # graph.clear_data - def clear_data - @data = [] - end - - - # This method processes the template with the data and - # config which has been set and returns the resulting SVG. - # - # This method will croak unless at least one data set has - # been added to the graph object. - # - # print graph.burn - def burn - raise "No data available" unless @data.size > 0 - - calculations if respond_to? :calculations - - start_svg - calculate_graph_dimensions - @foreground = Element.new( "g" ) - draw_graph - draw_titles - draw_legend - draw_data - @graph.add_element( @foreground ) - style - - data = "" - @doc.write( data, 0 ) - - if @config[:compress] - if @@__have_zlib - inp, out = IO.pipe - gz = Zlib::GzipWriter.new( out ) - gz.write data - gz.close - data = inp.read - else - data << ""; - end - end - - return data - end - - - # Set the height of the graph box, this is the total height - # of the SVG box created - not the graph it self which auto - # scales to fix the space. - attr_accessor :height - # Set the width of the graph box, this is the total width - # of the SVG box created - not the graph it self which auto - # scales to fix the space. - attr_accessor :width - # Set the path to an external stylesheet, set to '' if - # you want to revert back to using the defaut internal version. - # - # To create an external stylesheet create a graph using the - # default internal version and copy the stylesheet section to - # an external file and edit from there. - attr_accessor :style_sheet - # (Bool) Show the value of each element of data on the graph - attr_accessor :show_data_values - # The point at which the Y axis starts, defaults to '0', - # if set to nil it will default to the minimum data value. - attr_accessor :min_scale_value - # Whether to show labels on the X axis or not, defaults - # to true, set to false if you want to turn them off. - attr_accessor :show_x_labels - # This puts the X labels at alternative levels so if they - # are long field names they will not overlap so easily. - # Default it false, to turn on set to true. - attr_accessor :stagger_x_labels - # This puts the Y labels at alternative levels so if they - # are long field names they will not overlap so easily. - # Default it false, to turn on set to true. - attr_accessor :stagger_y_labels - # This turns the X axis labels by 90 degrees. - # Default it false, to turn on set to true. - attr_accessor :rotate_x_labels - # This turns the Y axis labels by 90 degrees. - # Default it false, to turn on set to true. - attr_accessor :rotate_y_labels - # How many "steps" to use between displayed X axis labels, - # a step of one means display every label, a step of two results - # in every other label being displayed (label label label), - # a step of three results in every third label being displayed - # (label label label) and so on. - attr_accessor :step_x_labels - # Whether to (when taking "steps" between X axis labels) step from - # the first label (i.e. always include the first label) or step from - # the X axis origin (i.e. start with a gap if step_x_labels is greater - # than one). - attr_accessor :step_include_first_x_label - # Whether to show labels on the Y axis or not, defaults - # to true, set to false if you want to turn them off. - attr_accessor :show_y_labels - # Ensures only whole numbers are used as the scale divisions. - # Default it false, to turn on set to true. This has no effect if - # scale divisions are less than 1. - attr_accessor :scale_integers - # This defines the gap between markers on the Y axis, - # default is a 10th of the max_value, e.g. you will have - # 10 markers on the Y axis. NOTE: do not set this too - # low - you are limited to 999 markers, after that the - # graph won't generate. - attr_accessor :scale_divisions - # Whether to show the title under the X axis labels, - # default is false, set to true to show. - attr_accessor :show_x_title - # What the title under X axis should be, e.g. 'Months'. - attr_accessor :x_title - # Whether to show the title under the Y axis labels, - # default is false, set to true to show. - attr_accessor :show_y_title - # Aligns writing mode for Y axis label. - # Defaults to :bt (Bottom to Top). - # Change to :tb (Top to Bottom) to reverse. - attr_accessor :y_title_text_direction - # What the title under Y axis should be, e.g. 'Sales in thousands'. - attr_accessor :y_title - # Whether to show a title on the graph, defaults - # to false, set to true to show. - attr_accessor :show_graph_title - # What the title on the graph should be. - attr_accessor :graph_title - # Whether to show a subtitle on the graph, defaults - # to false, set to true to show. - attr_accessor :show_graph_subtitle - # What the subtitle on the graph should be. - attr_accessor :graph_subtitle - # Whether to show a key, defaults to false, set to - # true if you want to show it. - attr_accessor :key - # Where the key should be positioned, defaults to - # :right, set to :bottom if you want to move it. - attr_accessor :key_position - # Set the font size (in points) of the data point labels - attr_accessor :font_size - # Set the font size of the X axis labels - attr_accessor :x_label_font_size - # Set the font size of the X axis title - attr_accessor :x_title_font_size - # Set the font size of the Y axis labels - attr_accessor :y_label_font_size - # Set the font size of the Y axis title - attr_accessor :y_title_font_size - # Set the title font size - attr_accessor :title_font_size - # Set the subtitle font size - attr_accessor :subtitle_font_size - # Set the key font size - attr_accessor :key_font_size - # Show guidelines for the X axis - attr_accessor :show_x_guidelines - # Show guidelines for the Y axis - attr_accessor :show_y_guidelines - # Do not use CSS if set to true. Many SVG viewers do not support CSS, but - # not using CSS can result in larger SVGs as well as making it impossible to - # change colors after the chart is generated. Defaults to false. - attr_accessor :no_css - # Add popups for the data points on some graphs - attr_accessor :add_popups - - - protected - - def sort( *arrys ) - sort_multiple( arrys ) - end - - # Overwrite configuration options with supplied options. Used - # by subclasses. - def init_with config - config.each { |key, value| - self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym - } - end - - attr_accessor :top_align, :top_font, :right_align, :right_font - - KEY_BOX_SIZE = 12 - - # Override this (and call super) to change the margin to the left - # of the plot area. Results in @border_left being set. - def calculate_left_margin - @border_left = 7 - # Check for Y labels - max_y_label_height_px = rotate_y_labels ? - y_label_font_size : - get_y_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * y_label_font_size * 0.6 - @border_left += max_y_label_height_px if show_y_labels - @border_left += max_y_label_height_px + 10 if stagger_y_labels - @border_left += y_title_font_size + 5 if show_y_title - end - - - # Calculates the width of the widest Y label. This will be the - # character height if the Y labels are rotated - def max_y_label_width_px - return font_size if rotate_y_labels - end - - - # Override this (and call super) to change the margin to the right - # of the plot area. Results in @border_right being set. - def calculate_right_margin - @border_right = 7 - if key and key_position == :right - val = keys.max { |a,b| a.length <=> b.length } - @border_right += val.length * key_font_size * 0.6 - @border_right += KEY_BOX_SIZE - @border_right += 10 # Some padding around the box - end - end - - - # Override this (and call super) to change the margin to the top - # of the plot area. Results in @border_top being set. - def calculate_top_margin - @border_top = 5 - @border_top += title_font_size if show_graph_title - @border_top += 5 - @border_top += subtitle_font_size if show_graph_subtitle - end - - - # Adds pop-up point information to a graph. - def add_popup( x, y, label ) - txt_width = label.length * font_size * 0.6 + 10 - tx = (x+txt_width > width ? x-5 : x+5) - t = @foreground.add_element( "text", { - "x" => tx.to_s, - "y" => (y - font_size).to_s, - "visibility" => "hidden", - }) - t.attributes["style"] = "fill: #000; "+ - (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") - t.text = label.to_s - t.attributes["id"] = t.object_id.to_s - - @foreground.add_element( "circle", { - "cx" => x.to_s, - "cy" => y.to_s, - "r" => "10", - "style" => "opacity: 0", - "onmouseover" => - "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )", - "onmouseout" => - "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )", - }) - - end - - - # Override this (and call super) to change the margin to the bottom - # of the plot area. Results in @border_bottom being set. - def calculate_bottom_margin - @border_bottom = 7 - if key and key_position == :bottom - @border_bottom += @data.size * (font_size + 5) - @border_bottom += 10 - end - if show_x_labels - max_x_label_height_px = (not rotate_x_labels) ? - x_label_font_size : - get_x_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * x_label_font_size * 0.6 - @border_bottom += max_x_label_height_px - @border_bottom += max_x_label_height_px + 10 if stagger_x_labels - end - @border_bottom += x_title_font_size + 5 if show_x_title - end - - - # Draws the background, axis, and labels. - def draw_graph - @graph = @root.add_element( "g", { - "transform" => "translate( #@border_left #@border_top )" - }) - - # Background - @graph.add_element( "rect", { - "x" => "0", - "y" => "0", - "width" => @graph_width.to_s, - "height" => @graph_height.to_s, - "class" => "graphBackground" - }) - - # Axis - @graph.add_element( "path", { - "d" => "M 0 0 v#@graph_height", - "class" => "axis", - "id" => "xAxis" - }) - @graph.add_element( "path", { - "d" => "M 0 #@graph_height h#@graph_width", - "class" => "axis", - "id" => "yAxis" - }) - - draw_x_labels - draw_y_labels - end - - - # Where in the X area the label is drawn - # Centered in the field, should be width/2. Start, 0. - def x_label_offset( width ) - 0 - end - - def make_datapoint_text( x, y, value, style="" ) - if show_data_values - @foreground.add_element( "text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "dataPointLabel", - "style" => "#{style} stroke: #fff; stroke-width: 2;" - }).text = value.to_s - text = @foreground.add_element( "text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "dataPointLabel" - }) - text.text = value.to_s - text.attributes["style"] = style if style.length > 0 - end - end - - - # Draws the X axis labels - def draw_x_labels - stagger = x_label_font_size + 5 - if show_x_labels - label_width = field_width - - count = 0 - for label in get_x_labels - if step_include_first_x_label == true then - step = count % step_x_labels - else - step = (count + 1) % step_x_labels - end - - if step == 0 then - text = @graph.add_element( "text" ) - text.attributes["class"] = "xAxisLabels" - text.text = label.to_s - - x = count * label_width + x_label_offset( label_width ) - y = @graph_height + x_label_font_size + 3 - t = 0 - (font_size / 2) - - if stagger_x_labels and count % 2 == 1 - y += stagger - @graph.add_element( "path", { - "d" => "M#{x} #@graph_height v#{stagger}", - "class" => "staggerGuideLine" - }) - end - - text.attributes["x"] = x.to_s - text.attributes["y"] = y.to_s - if rotate_x_labels - text.attributes["transform"] = - "rotate( 90 #{x} #{y-x_label_font_size} )"+ - " translate( 0 -#{x_label_font_size/4} )" - text.attributes["style"] = "text-anchor: start" - else - text.attributes["style"] = "text-anchor: middle" - end - end - - draw_x_guidelines( label_width, count ) if show_x_guidelines - count += 1 - end - end - end - - - # Where in the Y area the label is drawn - # Centered in the field, should be width/2. Start, 0. - def y_label_offset( height ) - 0 - end - - - def field_width - (@graph_width.to_f - font_size*2*right_font) / - (get_x_labels.length - right_align) - end - - - def field_height - (@graph_height.to_f - font_size*2*top_font) / - (get_y_labels.length - top_align) - end - - - # Draws the Y axis labels - def draw_y_labels - stagger = y_label_font_size + 5 - if show_y_labels - label_height = field_height - - count = 0 - y_offset = @graph_height + y_label_offset( label_height ) - y_offset += font_size/1.2 unless rotate_y_labels - for label in get_y_labels - y = y_offset - (label_height * count) - x = rotate_y_labels ? 0 : -3 - - if stagger_y_labels and count % 2 == 1 - x -= stagger - @graph.add_element( "path", { - "d" => "M#{x} #{y} h#{stagger}", - "class" => "staggerGuideLine" - }) - end - - text = @graph.add_element( "text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "yAxisLabels" - }) - text.text = label.to_s - if rotate_y_labels - text.attributes["transform"] = "translate( -#{font_size} 0 ) "+ - "rotate( 90 #{x} #{y} ) " - text.attributes["style"] = "text-anchor: middle" - else - text.attributes["y"] = (y - (y_label_font_size/2)).to_s - text.attributes["style"] = "text-anchor: end" - end - draw_y_guidelines( label_height, count ) if show_y_guidelines - count += 1 - end - end - end - - - # Draws the X axis guidelines - def draw_x_guidelines( label_height, count ) - if count != 0 - @graph.add_element( "path", { - "d" => "M#{label_height*count} 0 v#@graph_height", - "class" => "guideLines" - }) - end - end - - - # Draws the Y axis guidelines - def draw_y_guidelines( label_height, count ) - if count != 0 - @graph.add_element( "path", { - "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width", - "class" => "guideLines" - }) - end - end - - - # Draws the graph title and subtitle - def draw_titles - if show_graph_title - @root.add_element( "text", { - "x" => (width / 2).to_s, - "y" => (title_font_size).to_s, - "class" => "mainTitle" - }).text = graph_title.to_s - end - - if show_graph_subtitle - y_subtitle = show_graph_title ? - title_font_size + 10 : - subtitle_font_size - @root.add_element("text", { - "x" => (width / 2).to_s, - "y" => (y_subtitle).to_s, - "class" => "subTitle" - }).text = graph_subtitle.to_s - end - - if show_x_title - y = @graph_height + @border_top + x_title_font_size - if show_x_labels - y += x_label_font_size + 5 if stagger_x_labels - y += x_label_font_size + 5 - end - x = width / 2 - - @root.add_element("text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "xAxisTitle", - }).text = x_title.to_s - end - - if show_y_title - x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) - y = height / 2 - - text = @root.add_element("text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "yAxisTitle", - }) - text.text = y_title.to_s - if y_title_text_direction == :bt - text.attributes["transform"] = "rotate( -90, #{x}, #{y} )" - else - text.attributes["transform"] = "rotate( 90, #{x}, #{y} )" - end - end - end - - def keys - return @data.collect{ |d| d[:title] } - end - - # Draws the legend on the graph - def draw_legend - if key - group = @root.add_element( "g" ) - - key_count = 0 - for key_name in keys - y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5) - group.add_element( "rect", { - "x" => 0.to_s, - "y" => y_offset.to_s, - "width" => KEY_BOX_SIZE.to_s, - "height" => KEY_BOX_SIZE.to_s, - "class" => "key#{key_count+1}" - }) - group.add_element( "text", { - "x" => (KEY_BOX_SIZE + 5).to_s, - "y" => (y_offset + KEY_BOX_SIZE).to_s, - "class" => "keyText" - }).text = key_name.to_s - key_count += 1 - end - - case key_position - when :right - x_offset = @graph_width + @border_left + 10 - y_offset = @border_top + 20 - when :bottom - x_offset = @border_left + 20 - y_offset = @border_top + @graph_height + 5 - if show_x_labels - max_x_label_height_px = (not rotate_x_labels) ? - x_label_font_size : - get_x_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * x_label_font_size * 0.6 - x_label_font_size - y_offset += max_x_label_height_px - y_offset += max_x_label_height_px + 5 if stagger_x_labels - end - y_offset += x_title_font_size + 5 if show_x_title - end - group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" - end - end - - - private - - def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) - if lo < hi - p = partition(arrys,lo,hi) - sort_multiple(arrys, lo, p-1) - sort_multiple(arrys, p+1, hi) - end - arrys - end - - def partition( arrys, lo, hi ) - p = arrys[0][lo] - l = lo - z = lo+1 - while z <= hi - if arrys[0][z] < p - l += 1 - arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } - end - z += 1 - end - arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } - l - end - - def style - if no_css - styles = parse_css - @root.elements.each("//*[@class]") { |el| - cl = el.attributes["class"] - style = styles[cl] - style += el.attributes["style"] if el.attributes["style"] - el.attributes["style"] = style - } - end - end - - def parse_css - css = get_style - rv = {} - while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m - names_orig = names = $1 - css = $' - css =~ /([^}]+)\}/m - content = $1 - css = $' - - nms = [] - while names =~ /^\s*,?\s*\.(\w+)/ - nms << $1 - names = $' - end - - content = content.tr( "\n\t", " ") - for name in nms - current = rv[name] - current = current ? current+"; "+content : content - rv[name] = current.strip.squeeze(" ") - end - end - return rv - end - - - # Override and place code to add defs here - def add_defs defs - end - - - def start_svg - # Base document - @doc = Document.new - @doc << XMLDecl.new - @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + - %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} ) - if style_sheet && style_sheet != '' - @doc << Instruction.new( "xml-stylesheet", - %Q{href="#{style_sheet}" type="text/css"} ) - end - @root = @doc.add_element( "svg", { - "width" => width.to_s, - "height" => height.to_s, - "viewBox" => "0 0 #{width} #{height}", - "xmlns" => "http://www.w3.org/2000/svg", - "xmlns:xlink" => "http://www.w3.org/1999/xlink", - "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", - "a3:scriptImplementation" => "Adobe" - }) - @root << Comment.new( " "+"\\"*66 ) - @root << Comment.new( " Created with SVG::Graph " ) - @root << Comment.new( " SVG::Graph by Sean E. Russell " ) - @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ - " Leo Lapworth & Stephan Morgan " ) - @root << Comment.new( " "+"/"*66 ) - - defs = @root.add_element( "defs" ) - add_defs defs - if not(style_sheet && style_sheet != '') and !no_css - @root << Comment.new(" include default stylesheet if none specified ") - style = defs.add_element( "style", {"type"=>"text/css"} ) - style << CData.new( get_style ) - end - - @root << Comment.new( "SVG Background" ) - @root.add_element( "rect", { - "width" => width.to_s, - "height" => height.to_s, - "x" => "0", - "y" => "0", - "class" => "svgBackground" - }) - end - - - def calculate_graph_dimensions - calculate_left_margin - calculate_right_margin - calculate_bottom_margin - calculate_top_margin - @graph_width = width - @border_left - @border_right - @graph_height = height - @border_top - @border_bottom - end - - def get_style - return < 'get', :path => "/projects/5234/memberships.xml" }, + { :controller => 'members', :action => 'index', :project_id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'show', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/5234/memberships" }, + { :controller => 'members', :action => 'create', :project_id => '5234' } + ) + assert_routing( + { :method => 'post', :path => "/projects/5234/memberships.xml" }, + { :controller => 'members', :action => 'create', :project_id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/memberships/5234" }, + { :controller => 'members', :action => 'update', :id => '5234' } + ) + assert_routing( + { :method => 'put', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'update', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/memberships/5234" }, + { :controller => 'members', :action => 'destroy', :id => '5234' } + ) + assert_routing( + { :method => 'delete', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'destroy', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships/autocomplete" }, + { :controller => 'members', :action => 'autocomplete', :project_id => '5234' } + ) + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships/autocomplete.js" }, + { :controller => 'members', :action => 'autocomplete', :project_id => '5234', :format => 'js' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/08ba366b8c38c360363f9f7f4597d533502606b0.svn-base --- a/.svn/pristine/08/08ba366b8c38c360363f9f7f4597d533502606b0.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,312 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'workflows_controller' - -# Re-raise errors caught by the controller. -class WorkflowsController; def rescue_action(e) raise e end; end - -class WorkflowsControllerTest < ActionController::TestCase - fixtures :roles, :trackers, :workflows, :users, :issue_statuses - - def setup - @controller = WorkflowsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - @request.session[:user_id] = 1 # admin - end - - def test_index - get :index - assert_response :success - assert_template 'index' - - count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2') - assert_tag :tag => 'a', :content => count.to_s, - :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' } - end - - def test_get_edit - get :edit - assert_response :success - assert_template 'edit' - assert_not_nil assigns(:roles) - assert_not_nil assigns(:trackers) - end - - def test_get_edit_with_role_and_tracker - WorkflowTransition.delete_all - WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) - WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) - - get :edit, :role_id => 2, :tracker_id => 1 - assert_response :success - assert_template 'edit' - - # used status only - assert_not_nil assigns(:statuses) - assert_equal [2, 3, 5], assigns(:statuses).collect(&:id) - - # allowed transitions - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'issue_status[3][5][]', - :value => 'always', - :checked => 'checked' } - # not allowed - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'issue_status[3][2][]', - :value => 'always', - :checked => nil } - # unused - assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'issue_status[1][1][]' } - end - - def test_get_edit_with_role_and_tracker_and_all_statuses - WorkflowTransition.delete_all - - get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0' - assert_response :success - assert_template 'edit' - - assert_not_nil assigns(:statuses) - assert_equal IssueStatus.count, assigns(:statuses).size - - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'issue_status[1][1][]', - :value => 'always', - :checked => nil } - end - - def test_post_edit - post :edit, :role_id => 2, :tracker_id => 1, - :issue_status => { - '4' => {'5' => ['always']}, - '3' => {'1' => ['always'], '2' => ['always']} - } - assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' - - assert_equal 3, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) - assert_not_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) - assert_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4}) - end - - def test_post_edit_with_additional_transitions - post :edit, :role_id => 2, :tracker_id => 1, - :issue_status => { - '4' => {'5' => ['always']}, - '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']} - } - assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' - - assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) - - w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5}) - assert ! w.author - assert ! w.assignee - w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1}) - assert w.author - assert ! w.assignee - w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) - assert ! w.author - assert w.assignee - w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4}) - assert w.author - assert w.assignee - end - - def test_clear_workflow - assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0 - - post :edit, :role_id => 2, :tracker_id => 1 - assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) - end - - def test_get_permissions - get :permissions - - assert_response :success - assert_template 'permissions' - assert_not_nil assigns(:roles) - assert_not_nil assigns(:trackers) - end - - def test_get_permissions_with_role_and_tracker - WorkflowPermission.delete_all - WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') - WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') - WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') - - get :permissions, :role_id => 1, :tracker_id => 2 - assert_response :success - assert_template 'permissions' - - assert_select 'input[name=role_id][value=1]' - assert_select 'input[name=tracker_id][value=2]' - - # Required field - assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do - assert_select 'option[value=]' - assert_select 'option[value=][selected=selected]', 0 - assert_select 'option[value=readonly]', :text => 'Read-only' - assert_select 'option[value=readonly][selected=selected]', 0 - assert_select 'option[value=required]', :text => 'Required' - assert_select 'option[value=required][selected=selected]' - end - - # Read-only field - assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do - assert_select 'option[value=]' - assert_select 'option[value=][selected=selected]', 0 - assert_select 'option[value=readonly]', :text => 'Read-only' - assert_select 'option[value=readonly][selected=selected]' - assert_select 'option[value=required]', :text => 'Required' - assert_select 'option[value=required][selected=selected]', 0 - end - - # Other field - assert_select 'select[name=?]', 'permissions[due_date][3]' do - assert_select 'option[value=]' - assert_select 'option[value=][selected=selected]', 0 - assert_select 'option[value=readonly]', :text => 'Read-only' - assert_select 'option[value=readonly][selected=selected]', 0 - assert_select 'option[value=required]', :text => 'Required' - assert_select 'option[value=required][selected=selected]', 0 - end - end - - def test_get_permissions_with_required_custom_field_should_not_show_required_option - cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true) - - get :permissions, :role_id => 1, :tracker_id => 1 - assert_response :success - assert_template 'permissions' - - # Custom field that is always required - # The default option is "(Required)" - assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do - assert_select 'option[value=]' - assert_select 'option[value=readonly]', :text => 'Read-only' - assert_select 'option[value=required]', 0 - end - end - - def test_get_permissions_with_role_and_tracker_and_all_statuses - WorkflowTransition.delete_all - - get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0' - assert_response :success - assert_equal IssueStatus.sorted.all, assigns(:statuses) - end - - def test_post_permissions - WorkflowPermission.delete_all - - post :permissions, :role_id => 1, :tracker_id => 2, :permissions => { - 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''}, - 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''}, - 'due_date' => {'1' => '', '2' => '', '3' => ''}, - } - assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' - - workflows = WorkflowPermission.all - assert_equal 3, workflows.size - workflows.each do |workflow| - assert_equal 1, workflow.role_id - assert_equal 2, workflow.tracker_id - end - assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'} - assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'} - assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'} - end - - def test_post_permissions_should_clear_permissions - WorkflowPermission.delete_all - WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') - WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') - wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') - wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') - - post :permissions, :role_id => 1, :tracker_id => 2 - assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' - - workflows = WorkflowPermission.all - assert_equal 2, workflows.size - assert wf1.reload - assert wf2.reload - end - - def test_get_copy - get :copy - assert_response :success - assert_template 'copy' - assert_select 'select[name=source_tracker_id]' do - assert_select 'option[value=1]', :text => 'Bug' - end - assert_select 'select[name=source_role_id]' do - assert_select 'option[value=2]', :text => 'Developer' - end - assert_select 'select[name=?]', 'target_tracker_ids[]' do - assert_select 'option[value=3]', :text => 'Support request' - end - assert_select 'select[name=?]', 'target_role_ids[]' do - assert_select 'option[value=1]', :text => 'Manager' - end - end - - def test_post_copy_one_to_one - source_transitions = status_transitions(:tracker_id => 1, :role_id => 2) - - post :copy, :source_tracker_id => '1', :source_role_id => '2', - :target_tracker_ids => ['3'], :target_role_ids => ['1'] - assert_response 302 - assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1) - end - - def test_post_copy_one_to_many - source_transitions = status_transitions(:tracker_id => 1, :role_id => 2) - - post :copy, :source_tracker_id => '1', :source_role_id => '2', - :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] - assert_response 302 - assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1) - assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1) - assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3) - assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3) - end - - def test_post_copy_many_to_many - source_t2 = status_transitions(:tracker_id => 2, :role_id => 2) - source_t3 = status_transitions(:tracker_id => 3, :role_id => 2) - - post :copy, :source_tracker_id => 'any', :source_role_id => '2', - :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] - assert_response 302 - assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1) - assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1) - assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3) - assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3) - end - - # Returns an array of status transitions that can be compared - def status_transitions(conditions) - WorkflowTransition.find(:all, :conditions => conditions, - :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]} - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/08f914636b6cd922b89f9ce8cb2268cea53be558.svn-base --- a/.svn/pristine/08/08f914636b6cd922b89f9ce8cb2268cea53be558.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 used to represent the relations of an issue -class IssueRelations < Array - include Redmine::I18n - - def initialize(issue, *args) - @issue = issue - super(*args) - end - - def to_s(*args) - map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ') - end -end - -class IssueRelation < ActiveRecord::Base - belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' - belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' - - TYPE_RELATES = "relates" - TYPE_DUPLICATES = "duplicates" - TYPE_DUPLICATED = "duplicated" - TYPE_BLOCKS = "blocks" - TYPE_BLOCKED = "blocked" - TYPE_PRECEDES = "precedes" - TYPE_FOLLOWS = "follows" - TYPE_COPIED_TO = "copied_to" - TYPE_COPIED_FROM = "copied_from" - - TYPES = { - TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, - :order => 1, :sym => TYPE_RELATES }, - TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, - :order => 2, :sym => TYPE_DUPLICATED }, - TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, - :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, - TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, - :order => 4, :sym => TYPE_BLOCKED }, - TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, - :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, - TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, - :order => 6, :sym => TYPE_FOLLOWS }, - TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, - :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }, - TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, - :order => 8, :sym => TYPE_COPIED_FROM }, - TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to, - :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO } - }.freeze - - validates_presence_of :issue_from, :issue_to, :relation_type - validates_inclusion_of :relation_type, :in => TYPES.keys - validates_numericality_of :delay, :allow_nil => true - validates_uniqueness_of :issue_to_id, :scope => :issue_from_id - validate :validate_issue_relation - - attr_protected :issue_from_id, :issue_to_id - before_save :handle_issue_order - - def visible?(user=User.current) - (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user)) - end - - def deletable?(user=User.current) - visible?(user) && - ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) || - (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project))) - end - - def initialize(attributes=nil, *args) - super - if new_record? - if relation_type.blank? - self.relation_type = IssueRelation::TYPE_RELATES - end - end - end - - def validate_issue_relation - if issue_from && issue_to - errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id - unless issue_from.project_id == issue_to.project_id || - Setting.cross_project_issue_relations? - errors.add :issue_to_id, :not_same_project - end - # detect circular dependencies depending wether the relation should be reversed - if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] - errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to - else - errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from - end - if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to) - errors.add :base, :cant_link_an_issue_with_a_descendant - end - end - end - - def other_issue(issue) - (self.issue_from_id == issue.id) ? issue_to : issue_from - end - - # Returns the relation type for +issue+ - def relation_type_for(issue) - if TYPES[relation_type] - if self.issue_from_id == issue.id - relation_type - else - TYPES[relation_type][:sym] - end - end - end - - def label_for(issue) - TYPES[relation_type] ? - TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : - :unknow - end - - def css_classes_for(issue) - "rel-#{relation_type_for(issue)}" - end - - def handle_issue_order - reverse_if_needed - - if TYPE_PRECEDES == relation_type - self.delay ||= 0 - else - self.delay = nil - end - set_issue_to_dates - end - - def set_issue_to_dates - soonest_start = self.successor_soonest_start - if soonest_start && issue_to - issue_to.reschedule_on!(soonest_start) - end - end - - def successor_soonest_start - if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && - (issue_from.start_date || issue_from.due_date) - (issue_from.due_date || issue_from.start_date) + 1 + delay - end - end - - def <=>(relation) - r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] - r == 0 ? id <=> relation.id : r - end - - private - - # Reverses the relation if needed so that it gets stored in the proper way - # Should not be reversed before validation so that it can be displayed back - # as entered on new relation form - def reverse_if_needed - if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] - issue_tmp = issue_to - self.issue_to = issue_from - self.issue_from = issue_tmp - self.relation_type = TYPES[relation_type][:reverse] - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/08/08fb7d1dbcb8591c64d100b93daff66b95400b98.svn-base --- a/.svn/pristine/08/08fb7d1dbcb8591c64d100b93daff66b95400b98.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,185 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 DefaultData - class DataAlreadyLoaded < Exception; end - - module Loader - include Redmine::I18n - - class << self - # Returns true if no data is already loaded in the database - # otherwise false - def no_data? - !Role.find(:first, :conditions => {:builtin => 0}) && - !Tracker.find(:first) && - !IssueStatus.find(:first) && - !Enumeration.find(:first) - end - - # Loads the default data - # Raises a RecordNotSaved exception if something goes wrong - def load(lang=nil) - raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data? - set_language_if_valid(lang) - - Role.transaction do - # Roles - manager = Role.create! :name => l(:default_role_manager), - :issues_visibility => 'all', - :position => 1 - manager.permissions = manager.setable_permissions.collect {|p| p.name} - manager.save! - - developer = Role.create! :name => l(:default_role_developer), - :position => 2, - :permissions => [:manage_versions, - :manage_categories, - :view_issues, - :add_issues, - :edit_issues, - :view_private_notes, - :set_notes_private, - :manage_issue_relations, - :manage_subtasks, - :add_issue_notes, - :save_queries, - :view_gantt, - :view_calendar, - :log_time, - :view_time_entries, - :comment_news, - :view_documents, - :view_wiki_pages, - :view_wiki_edits, - :edit_wiki_pages, - :delete_wiki_pages, - :add_messages, - :edit_own_messages, - :view_files, - :manage_files, - :browse_repository, - :view_changesets, - :commit_access, - :manage_related_issues] - - reporter = Role.create! :name => l(:default_role_reporter), - :position => 3, - :permissions => [:view_issues, - :add_issues, - :add_issue_notes, - :save_queries, - :view_gantt, - :view_calendar, - :log_time, - :view_time_entries, - :comment_news, - :view_documents, - :view_wiki_pages, - :view_wiki_edits, - :add_messages, - :edit_own_messages, - :view_files, - :browse_repository, - :view_changesets] - - Role.non_member.update_attribute :permissions, [:view_issues, - :add_issues, - :add_issue_notes, - :save_queries, - :view_gantt, - :view_calendar, - :view_time_entries, - :comment_news, - :view_documents, - :view_wiki_pages, - :view_wiki_edits, - :add_messages, - :view_files, - :browse_repository, - :view_changesets] - - Role.anonymous.update_attribute :permissions, [:view_issues, - :view_gantt, - :view_calendar, - :view_time_entries, - :view_documents, - :view_wiki_pages, - :view_wiki_edits, - :view_files, - :browse_repository, - :view_changesets] - - # Trackers - Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1) - Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2) - Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3) - - # Issue statuses - new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1) - in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2) - resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3) - feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4) - closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5) - rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6) - - # Workflow - Tracker.find(:all).each { |t| - IssueStatus.find(:all).each { |os| - IssueStatus.find(:all).each { |ns| - WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns - } - } - } - - Tracker.find(:all).each { |t| - [new, in_progress, resolved, feedback].each { |os| - [in_progress, resolved, feedback, closed].each { |ns| - WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns - } - } - } - - Tracker.find(:all).each { |t| - [new, in_progress, resolved, feedback].each { |os| - [closed].each { |ns| - WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns - } - } - WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id) - } - - # Enumerations - IssuePriority.create!(:name => l(:default_priority_low), :position => 1) - IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true) - IssuePriority.create!(:name => l(:default_priority_high), :position => 3) - IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4) - IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5) - - DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1) - DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2) - - TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1) - TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2) - end - true - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09050a06606068974830186c1100d7dceca803ff.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09050a06606068974830186c1100d7dceca803ff.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,31 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::NotifiableTest < ActiveSupport::TestCase + def setup + end + + def test_all + assert_equal 12, Redmine::Notifiable.all.length + + %w(issue_added issue_updated issue_note_added issue_status_updated issue_priority_updated news_added news_comment_added document_added file_added message_posted wiki_content_added wiki_content_updated).each do |notifiable| + assert Redmine::Notifiable.all.collect(&:name).include?(notifiable), "missing #{notifiable}" + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/0930e7dcd668d1bc526e085d51f359ac0b2bfdf3.svn-base --- a/.svn/pristine/09/0930e7dcd668d1bc526e085d51f359ac0b2bfdf3.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -api.user do - api.id @user.id - api.login @user.login if User.current.admin? - api.firstname @user.firstname - api.lastname @user.lastname - api.mail @user.mail if User.current.admin? || !@user.pref.hide_mail - api.created_on @user.created_on - api.last_login_on @user.last_login_on - - render_api_custom_values @user.visible_custom_field_values, api - - api.array :groups do |groups| - @user.groups.each do |group| - api.group :id => group.id, :name => group.name - end - end if User.current.admin? && include_in_api_response?('groups') - - api.array :memberships do - @memberships.each do |membership| - api.membership do - api.id membership.id - api.project :id => membership.project.id, :name => membership.project.name - api.array :roles do - membership.member_roles.each do |member_role| - if member_role.role - attrs = {:id => member_role.role.id, :name => member_role.role.name} - attrs.merge!(:inherited => true) if member_role.inherited_from.present? - api.role attrs - end - end - end - end if membership.project - end - end if include_in_api_response?('memberships') && @memberships -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09319bb04693b301a469674c2043f8c089fbdcbb.svn-base --- a/.svn/pristine/09/09319bb04693b301a469674c2043f8c089fbdcbb.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4349 +0,0 @@ -#============================================================+ -# 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 -# - -@@version = "1.53.0.TC031" -@@fpdf_charwidths = {} - -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 - - 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):
    • P or Portrait (default)
    • L or Landscape
    - # @param string :unit User measure unit. Possible values are:
    • pt: point
    • mm: millimeter (default)
    • cm: centimeter
    • in: inch

    A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit. - # @param mixed :format The format used for pages. It can be either one of the following values (case insensitive) or a custom format in the form of a two-element array containing the width and the height (expressed in the unit given by unit).
    • 4A0
    • 2A0
    • A0
    • A1
    • A2
    • A3
    • A4 (default)
    • A5
    • A6
    • A7
    • A8
    • A9
    • A10
    • B0
    • B1
    • B2
    • B3
    • B4
    • B5
    • B6
    • B7
    • B8
    • B9
    • B10
    • C0
    • C1
    • C2
    • C3
    • C4
    • C5
    • C6
    • C7
    • C8
    • C9
    • C10
    • RA0
    • RA1
    • RA2
    • RA3
    • RA4
    • SRA0
    • SRA1
    • SRA2
    • SRA3
    • SRA4
    • LETTER
    • LEGAL
    • EXECUTIVE
    • FOLIO
    - # @param boolean :unicode TRUE means that the input text is unicode (default = true) - # @param String :encoding charset encoding; default is UTF-8 - # - def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, encoding = "UTF-8") - - # Set internal character encoding to ASCII# - #FIXME 2007-05-25 (EJM) Level=0 - - # if (respond_to?("mb_internal_encoding") and mb_internal_encoding()) - # @internal_encoding = mb_internal_encoding(); - # mb_internal_encoding("ASCII"); - # } - - #Some checks - dochecks(); - - #Initialization of properties - @barcode ||= false - @buffer ||= '' - @diffs ||= [] - @color_flag ||= false - @default_table_columns ||= 4 - @table_columns ||= 0 - @max_table_columns ||= [] - @tr_id ||= 0 - @max_td_page ||= [] - @max_td_y ||= [] - @t_columns ||= 0 - @default_font ||= "FreeSans" if unicode - @default_font ||= "Helvetica" - @draw_color ||= '0 G' - @encoding ||= "UTF-8" - @fill_color ||= '0 g' - @fonts ||= {} - @font_family ||= '' - @font_files ||= {} - @font_style ||= '' - @font_size ||= 12 - @font_size_pt ||= 12 - @header_width ||= 0 - @header_logo ||= "" - @header_logo_width ||= 30 - @header_title ||= "" - @header_string ||= "" - @images ||= {} - @img_scale ||= 1 - @in_footer ||= false - @is_unicode = unicode - @lasth ||= 0 - @links ||= [] - @list_ordered ||= [] - @list_count ||= [] - @li_spacer ||= "" - @li_count ||= 0 - @spacer ||= "" - @quote_count ||= 0 - @prevquote_count ||= 0 - @quote_top ||= [] - @quote_page ||= [] - @n ||= 2 - @offsets ||= [] - @orientation_changes ||= [] - @page ||= 0 - @page_links ||= {} - @pages ||= [] - @pdf_version ||= "1.3" - @prevfill_color ||= [255,255,255] - @prevtext_color ||= [0,0,0] - @print_header ||= false - @print_footer ||= false - @state ||= 0 - @tableborder ||= 0 - @tdbegin ||= false - @tdtext ||= '' - @tdwidth ||= 0 - @tdheight ||= 0 - @tdalign ||= "L" - @tdfill ||= 0 - @tempfontsize ||= 10 - @text_color ||= '0 g' - @underline ||= false - @deleted ||= false - @ws ||= 0 - - #Standard Unicode fonts - @core_fonts = { - 'courier'=>'Courier', - 'courierB'=>'Courier-Bold', - 'courierI'=>'Courier-Oblique', - 'courierBI'=>'Courier-BoldOblique', - 'helvetica'=>'Helvetica', - 'helveticaB'=>'Helvetica-Bold', - 'helveticaI'=>'Helvetica-Oblique', - 'helveticaBI'=>'Helvetica-BoldOblique', - 'times'=>'Times-Roman', - 'timesB'=>'Times-Bold', - 'timesI'=>'Times-Italic', - 'timesBI'=>'Times-BoldItalic', - 'symbol'=>'Symbol', - 'zapfdingbats'=>'ZapfDingbats'} - - #Scale factor - case unit.downcase - when 'pt' ; @k=1 - when 'mm' ; @k=72/25.4 - when 'cm' ; @k=72/2.54 - when 'in' ; @k=72 - else Error("Incorrect unit: #{unit}") - end - - #Page format - if format.is_a?(String) - # Page formats (45 standard ISO paper formats and 4 american common formats). - # Paper cordinates are calculated in this way: (inches# 72) where (1 inch = 2.54 cm) - case (format.upcase) - when '4A0' ; format = [4767.87,6740.79] - when '2A0' ; format = [3370.39,4767.87] - when 'A0' ; format = [2383.94,3370.39] - when 'A1' ; format = [1683.78,2383.94] - when 'A2' ; format = [1190.55,1683.78] - when 'A3' ; format = [841.89,1190.55] - when 'A4' ; format = [595.28,841.89] # ; default - when 'A5' ; format = [419.53,595.28] - when 'A6' ; format = [297.64,419.53] - when 'A7' ; format = [209.76,297.64] - when 'A8' ; format = [147.40,209.76] - when 'A9' ; format = [104.88,147.40] - when 'A10' ; format = [73.70,104.88] - when 'B0' ; format = [2834.65,4008.19] - when 'B1' ; format = [2004.09,2834.65] - when 'B2' ; format = [1417.32,2004.09] - when 'B3' ; format = [1000.63,1417.32] - when 'B4' ; format = [708.66,1000.63] - when 'B5' ; format = [498.90,708.66] - when 'B6' ; format = [354.33,498.90] - when 'B7' ; format = [249.45,354.33] - when 'B8' ; format = [175.75,249.45] - when 'B9' ; format = [124.72,175.75] - when 'B10' ; format = [87.87,124.72] - when 'C0' ; format = [2599.37,3676.54] - when 'C1' ; format = [1836.85,2599.37] - when 'C2' ; format = [1298.27,1836.85] - when 'C3' ; format = [918.43,1298.27] - when 'C4' ; format = [649.13,918.43] - when 'C5' ; format = [459.21,649.13] - when 'C6' ; format = [323.15,459.21] - when 'C7' ; format = [229.61,323.15] - when 'C8' ; format = [161.57,229.61] - when 'C9' ; format = [113.39,161.57] - when 'C10' ; format = [79.37,113.39] - when 'RA0' ; format = [2437.80,3458.27] - when 'RA1' ; format = [1729.13,2437.80] - when 'RA2' ; format = [1218.90,1729.13] - when 'RA3' ; format = [864.57,1218.90] - when 'RA4' ; format = [609.45,864.57] - when 'SRA0' ; format = [2551.18,3628.35] - when 'SRA1' ; format = [1814.17,2551.18] - when 'SRA2' ; format = [1275.59,1814.17] - when 'SRA3' ; format = [907.09,1275.59] - when 'SRA4' ; format = [637.80,907.09] - when 'LETTER' ; format = [612.00,792.00] - when 'LEGAL' ; format = [612.00,1008.00] - when 'EXECUTIVE' ; format = [521.86,756.00] - when 'FOLIO' ; format = [612.00,936.00] - #else then Error("Unknown page format: #{format}" - end - @fw_pt = format[0] - @fh_pt = format[1] - else - @fw_pt = format[0]*@k - @fh_pt = format[1]*@k - end - - @fw = @fw_pt/@k - @fh = @fh_pt/@k - - #Page orientation - orientation = orientation.downcase - if orientation == 'p' or orientation == 'portrait' - @def_orientation = 'P' - @w_pt = @fw_pt - @h_pt = @fh_pt - elsif orientation == 'l' or orientation == 'landscape' - @def_orientation = 'L' - @w_pt = @fh_pt - @h_pt = @fw_pt - else - Error("Incorrect orientation: #{orientation}") - end - - @fw = @w_pt/@k - @fh = @h_pt/@k - - @cur_orientation = @def_orientation - @w = @w_pt/@k - @h = @h_pt/@k - #Page margins (1 cm) - margin = 28.35/@k - SetMargins(margin, margin) - #Interior cell margin (1 mm) - @c_margin = margin / 10 - #Line width (0.2 mm) - @line_width = 0.567 / @k - #Automatic page break - SetAutoPageBreak(true, 2 * margin) - #Full width display mode - SetDisplayMode('fullwidth') - #Compression - SetCompression(true) - #Set default PDF version number - @pdf_version = "1.3" - - @encoding = encoding - @b = 0 - @i = 0 - @u = 0 - @href = '' - @fontlist = ["arial", "times", "courier", "helvetica", "symbol"] - @issetfont = false - @issetcolor = false - - SetFillColor(200, 200, 200, true) - SetTextColor(0, 0, 0, true) - end - - # - # Set the image scale. - # @param float :scale image scale. - # @author Nicola Asuni - # @since 1.5.2 - # - def SetImageScale(scale) - @img_scale = scale; - end - alias_method :set_image_scale, :SetImageScale - - # - # Returns the image scale. - # @return float image scale. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetImageScale() - return @img_scale; - end - alias_method :get_image_scale, :GetImageScale - - # - # Returns the page width in units. - # @return int page width. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetPageWidth() - return @w; - end - alias_method :get_page_width, :GetPageWidth - - # - # Returns the page height in units. - # @return int page height. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetPageHeight() - return @h; - end - alias_method :get_page_height, :GetPageHeight - - # - # Returns the page break margin. - # @return int page break margin. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetBreakMargin() - return @b_margin; - end - alias_method :get_break_margin, :GetBreakMargin - - # - # Returns the scale factor (number of points in user unit). - # @return int scale factor. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetScaleFactor() - return @k; - end - alias_method :get_scale_factor, :GetScaleFactor - - # - # Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change them. - # @param float :left Left margin. - # @param float :top Top margin. - # @param float :right Right margin. Default value is the left one. - # @since 1.0 - # @see SetLeftMargin(), SetTopMargin(), SetRightMargin(), SetAutoPageBreak() - # - def SetMargins(left, top, right=-1) - #Set left, top and right margins - @l_margin = left - @t_margin = top - if (right == -1) - right = left - end - @r_margin = right - end - alias_method :set_margins, :SetMargins - - # - # Defines the left margin. The method can be called before creating the first page. If the current abscissa gets out of page, it is brought back to the margin. - # @param float :margin The margin. - # @since 1.4 - # @see SetTopMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() - # - def SetLeftMargin(margin) - #Set left margin - @l_margin = margin - if ((@page>0) and (@x < margin)) - @x = margin - end - end - alias_method :set_left_margin, :SetLeftMargin - - # - # Defines the top margin. The method can be called before creating the first page. - # @param float :margin The margin. - # @since 1.5 - # @see SetLeftMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() - # - def SetTopMargin(margin) - #Set top margin - @t_margin = margin - end - alias_method :set_top_margin, :SetTopMargin - - # - # Defines the right margin. The method can be called before creating the first page. - # @param float :margin The margin. - # @since 1.5 - # @see SetLeftMargin(), SetTopMargin(), SetAutoPageBreak(), SetMargins() - # - def SetRightMargin(margin) - #Set right margin - @r_margin = margin - end - alias_method :set_right_margin, :SetRightMargin - - # - # Enables or disables the automatic page breaking mode. When enabling, the second parameter is the distance from the bottom of the page that defines the triggering limit. By default, the mode is on and the margin is 2 cm. - # @param boolean :auto Boolean indicating if mode should be on or off. - # @param float :margin Distance from the bottom of the page. - # @since 1.0 - # @see Cell(), MultiCell(), AcceptPageBreak() - # - def SetAutoPageBreak(auto, margin=0) - #Set auto page break mode and triggering margin - @auto_page_break = auto - @b_margin = margin - @page_break_trigger = @h - margin - end - alias_method :set_auto_page_break, :SetAutoPageBreak - - # - # Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a specific zooming factor or use viewer default (configured in the Preferences menu of Acrobat). The page layout can be specified too: single at once, continuous display, two columns or viewer default. By default, documents use the full width mode with continuous display. - # @param mixed :zoom The zoom to use. It can be one of the following string values or a number indicating the zooming factor to use.
    • fullpage: displays the entire page on screen
    • fullwidth: uses maximum width of window
    • real: uses real size (equivalent to 100% zoom)
    • default: uses viewer default mode
    - # @param string :layout The page layout. Possible values are:
    • single: displays one page at once
    • continuous: displays pages continuously (default)
    • two: displays two pages on two columns
    • default: uses viewer default mode
    - # @since 1.2 - # - def SetDisplayMode(zoom, layout = 'continuous') - #Set display mode in viewer - if (zoom == 'fullpage' or zoom == 'fullwidth' or zoom == 'real' or zoom == 'default' or !zoom.is_a?(String)) - @zoom_mode = zoom - else - Error("Incorrect zoom display mode: #{zoom}") - end - if (layout == 'single' or layout == 'continuous' or layout == 'two' or layout == 'default') - @layout_mode = layout - else - Error("Incorrect layout display mode: #{layout}") - end - end - alias_method :set_display_mode, :SetDisplayMode - - # - # Activates or deactivates page compression. When activated, the internal representation of each page is compressed, which leads to a compression ratio of about 2 for the resulting document. Compression is on by default. - # Note: the Zlib extension is required for this feature. If not present, compression will be turned off. - # @param boolean :compress Boolean indicating if compression must be enabled. - # @since 1.4 - # - def SetCompression(compress) - #Set page compression - if (respond_to?('gzcompress')) - @compress = compress - else - @compress = false - end - end - alias_method :set_compression, :SetCompression - - # - # Defines the title of the document. - # @param string :title The title. - # @since 1.2 - # @see SetAuthor(), SetCreator(), SetKeywords(), SetSubject() - # - def SetTitle(title) - #Title of document - @title = title - end - alias_method :set_title, :SetTitle - - # - # Defines the subject of the document. - # @param string :subject The subject. - # @since 1.2 - # @see SetAuthor(), SetCreator(), SetKeywords(), SetTitle() - # - def SetSubject(subject) - #Subject of document - @subject = subject - end - alias_method :set_subject, :SetSubject - - # - # Defines the author of the document. - # @param string :author The name of the author. - # @since 1.2 - # @see SetCreator(), SetKeywords(), SetSubject(), SetTitle() - # - def SetAuthor(author) - #Author of document - @author = author - end - alias_method :set_author, :SetAuthor - - # - # Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'. - # @param string :keywords The list of keywords. - # @since 1.2 - # @see SetAuthor(), SetCreator(), SetSubject(), SetTitle() - # - def SetKeywords(keywords) - #Keywords of document - @keywords = keywords - end - alias_method :set_keywords, :SetKeywords - - # - # Defines the creator of the document. This is typically the name of the application that generates the PDF. - # @param string :creator The name of the creator. - # @since 1.2 - # @see SetAuthor(), SetKeywords(), SetSubject(), SetTitle() - # - def SetCreator(creator) - #Creator of document - @creator = creator - end - alias_method :set_creator, :SetCreator - - # - # Defines an alias for the total number of pages. It will be substituted as the document is closed.
    - # Example:
    - #
    -	# 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):
    • P or Portrait
    • L or Landscape
    The default value is the one passed to the constructor. - # @since 1.0 - # @see TCPDF(), Header(), Footer(), SetMargins() - # - def AddPage(orientation='') - #Start a new page - if (@state==0) - Open(); - end - family=@font_family; - style=@font_style + (@underline ? 'U' : '') + (@deleted ? 'D' : ''); - size=@font_size_pt; - lw=@line_width; - dc=@draw_color; - fc=@fill_color; - tc=@text_color; - cf=@color_flag; - if (@page>0) - #Page footer - @in_footer=true; - Footer(); - @in_footer=false; - #Close page - endpage(); - end - #Start new page - beginpage(orientation); - #Set line cap style to square - out('2 J'); - #Set line width - @line_width = lw; - out(sprintf('%.2f w', lw*@k)); - #Set font - if (family) - SetFont(family, style, size); - end - #Set colors - @draw_color = dc; - if (dc!='0 G') - out(dc); - end - @fill_color = fc; - if (fc!='0 g') - out(fc); - end - @text_color = tc; - @color_flag = cf; - #Page header - Header(); - #Restore line width - if (@line_width != lw) - @line_width = lw; - out(sprintf('%.2f w', lw*@k)); - end - #Restore font - if (family) - SetFont(family, style, size); - end - #Restore colors - if (@draw_color != dc) - @draw_color = dc; - out(dc); - end - if (@fill_color != fc) - @fill_color = fc; - out(fc); - end - @text_color = tc; - @color_flag = cf; - end - alias_method :add_page, :AddPage - - # - # Rotate object. - # @param float :angle angle in degrees for counter-clockwise rotation - # @param int :x abscissa of the rotation center. Default is current x position - # @param int :y ordinate of the rotation center. Default is current y position - # - def Rotate(angle, x="", y="") - - if (x == '') - x = @x; - end - - if (y == '') - y = @y; - end - - if (@rtl) - x = @w - x; - angle = -@angle; - end - - y = (@h - y) * @k; - x *= @k; - - # calculate elements of transformation matrix - tm = [] - tm[0] = ::Math::cos(deg2rad(angle)); - tm[1] = ::Math::sin(deg2rad(angle)); - tm[2] = -tm[1]; - tm[3] = tm[0]; - tm[4] = x + tm[1] * y - tm[0] * x; - tm[5] = y - tm[0] * y - tm[1] * x; - - # generate the transformation matrix - Transform(tm); - end - alias_method :rotate, :Rotate - - # - # Starts a 2D tranformation saving current graphic state. - # This function must be called before scaling, mirroring, translation, rotation and skewing. - # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. - # - def StartTransform - out('q'); - end - alias_method :start_transform, :StartTransform - - # - # Stops a 2D tranformation restoring previous graphic state. - # This function must be called after scaling, mirroring, translation, rotation and skewing. - # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. - # - def StopTransform - out('Q'); - end - alias_method :stop_transform, :StopTransform - - # - # Apply graphic transformations. - # @since 2.1.000 (2008-01-07) - # @see StartTransform(), StopTransform() - # - def Transform(tm) - x = out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', tm[0], tm[1], tm[2], tm[3], tm[4], tm[5])); - end - alias_method :transform, :Transform - - # - # Set header data. - # @param string :ln header image logo - # @param string :lw header image logo width in mm - # @param string :ht string to print as title on document header - # @param string :hs string to print on document header - # - def SetHeaderData(ln="", lw=0, ht="", hs="") - @header_logo = ln || "" - @header_logo_width = lw || 0 - @header_title = ht || "" - @header_string = hs || "" - end - alias_method :set_header_data, :SetHeaderData - - # - # Set header margin. - # (minimum distance between header and top page margin) - # @param int :hm distance in millimeters - # - def SetHeaderMargin(hm=10) - @header_margin = hm; - end - alias_method :set_header_margin, :SetHeaderMargin - - # - # Set footer margin. - # (minimum distance between footer and bottom page margin) - # @param int :fm distance in millimeters - # - def SetFooterMargin(fm=10) - @footer_margin = fm; - end - alias_method :set_footer_margin, :SetFooterMargin - - # - # Set a flag to print page header. - # @param boolean :val set to true to print the page header (default), false otherwise. - # - def SetPrintHeader(val=true) - @print_header = val; - end - alias_method :set_print_header, :SetPrintHeader - - # - # Set a flag to print page footer. - # @param boolean :value set to true to print the page footer (default), false otherwise. - # - def SetPrintFooter(val=true) - @print_footer = val; - end - alias_method :set_print_footer, :SetPrintFooter - - # - # This method is used to render the page header. - # It is automatically called by AddPage() and could be overwritten in your own inherited class. - # - def Header() - if (@print_header) - if (@original_l_margin.nil?) - @original_l_margin = @l_margin; - end - if (@original_r_margin.nil?) - @original_r_margin = @r_margin; - end - - #set current position - SetXY(@original_l_margin, @header_margin); - - if ((@header_logo) and (@header_logo != @@k_blank_image)) - Image(@header_logo, @original_l_margin, @header_margin, @header_logo_width); - else - @img_rb_y = GetY(); - end - - cell_height = ((@@k_cell_height_ratio * @header_font[2]) / @k).round(2) - - header_x = @original_l_margin + (@header_logo_width * 1.05); #set left margin for text data cell - - # header title - SetFont(@header_font[0], 'B', @header_font[2] + 1); - SetX(header_x); - Cell(@header_width, cell_height, @header_title, 0, 1, 'L'); - - # header string - SetFont(@header_font[0], @header_font[1], @header_font[2]); - SetX(header_x); - MultiCell(@header_width, cell_height, @header_string, 0, 'L', 0); - - # print an ending header line - if (@header_width) - #set style for cell border - SetLineWidth(0.3); - SetDrawColor(0, 0, 0); - SetY(1 + (@img_rb_y > GetY() ? @img_rb_y : GetY())); - SetX(@original_l_margin); - Cell(0, 0, '', 'T', 0, 'C'); - end - - #restore position - SetXY(@original_l_margin, @t_margin); - end - end - alias_method :header, :Header - - # - # This method is used to render the page footer. - # It is automatically called by AddPage() and could be overwritten in your own inherited class. - # - def Footer() - if (@print_footer) - - if (@original_l_margin.nil?) - @original_l_margin = @l_margin; - end - if (@original_r_margin.nil?) - @original_r_margin = @r_margin; - end - - #set font - SetFont(@footer_font[0], @footer_font[1] , @footer_font[2]); - #set style for cell border - line_width = 0.3; - SetLineWidth(line_width); - SetDrawColor(0, 0, 0); - - footer_height = ((@@k_cell_height_ratio * @footer_font[2]) / @k).round; #footer height, was , 2) - #get footer y position - footer_y = @h - @footer_margin - footer_height; - #set current position - SetXY(@original_l_margin, footer_y); - - #print document barcode - if (@barcode) - Ln(); - barcode_width = ((@w - @original_l_margin - @original_r_margin)).round; #max width - writeBarcode(@original_l_margin, footer_y + line_width, barcode_width, footer_height - line_width, "C128B", false, false, 2, @barcode); - end - - SetXY(@original_l_margin, footer_y); - - #Print page number - Cell(0, footer_height, @l['w_page'] + " " + PageNo().to_s + ' / {nb}', 'T', 0, 'R'); - end - end - alias_method :footer, :Footer - - # - # Returns the current page number. - # @return int page number - # @since 1.0 - # @see alias_nb_pages() - # - def PageNo() - #Get current page number - return @page; - end - alias_method :page_no, :PageNo - - # - # Defines the color used for all drawing operations (lines, rectangles and cell borders). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. - # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 - # @param int :g Green component (between 0 and 255) - # @param int :b Blue component (between 0 and 255) - # @since 1.3 - # @see SetFillColor(), SetTextColor(), Line(), Rect(), Cell(), MultiCell() - # - def SetDrawColor(r, g=-1, b=-1) - #Set color for all stroking operations - if ((r==0 and g==0 and b==0) or g==-1) - @draw_color=sprintf('%.3f G', r/255.0); - else - @draw_color=sprintf('%.3f %.3f %.3f RG', r/255.0, g/255.0, b/255.0); - end - if (@page>0) - out(@draw_color); - end - end - alias_method :set_draw_color, :SetDrawColor - - # - # Defines the color used for all filling operations (filled rectangles and cell backgrounds). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. - # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 - # @param int :g Green component (between 0 and 255) - # @param int :b Blue component (between 0 and 255) - # @param boolean :storeprev if true stores the RGB array on :prevfill_color variable. - # @since 1.3 - # @see SetDrawColor(), SetTextColor(), Rect(), Cell(), MultiCell() - # - def SetFillColor(r, g=-1, b=-1, storeprev=false) - #Set color for all filling operations - if ((r==0 and g==0 and b==0) or g==-1) - @fill_color=sprintf('%.3f g', r/255.0); - else - @fill_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); - end - @color_flag=(@fill_color!=@text_color); - if (@page>0) - out(@fill_color); - end - if (storeprev) - # store color as previous value - @prevfill_color = [r, g, b] - end - end - alias_method :set_fill_color, :SetFillColor - - # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors - def SetCmykFillColor(c, m, y, k, storeprev=false) - #Set color for all filling operations - @fill_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); - @color_flag=(@fill_color!=@text_color); - if (storeprev) - # store color as previous value - @prevtext_color = [c, m, y, k] - end - if (@page>0) - out(@fill_color); - end - end - alias_method :set_cmyk_fill_color, :SetCmykFillColor - - # - # Defines the color used for text. It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. - # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 - # @param int :g Green component (between 0 and 255) - # @param int :b Blue component (between 0 and 255) - # @param boolean :storeprev if true stores the RGB array on :prevtext_color variable. - # @since 1.3 - # @see SetDrawColor(), SetFillColor(), Text(), Cell(), MultiCell() - # - def SetTextColor(r, g=-1, b=-1, storeprev=false) - #Set color for text - if ((r==0 and :g==0 and :b==0) or :g==-1) - @text_color=sprintf('%.3f g', r/255.0); - else - @text_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); - end - @color_flag=(@fill_color!=@text_color); - if (storeprev) - # store color as previous value - @prevtext_color = [r, g, b] - end - end - alias_method :set_text_color, :SetTextColor - - # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors - def SetCmykTextColor(c, m, y, k, storeprev=false) - #Set color for text - @text_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); - @color_flag=(@fill_color!=@text_color); - if (storeprev) - # store color as previous value - @prevtext_color = [c, m, y, k] - end - end - alias_method :set_cmyk_text_color, :SetCmykTextColor - - # - # Returns the length of a string in user unit. A font must be selected.
    - # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02] - # @param string :s The string whose length is to be computed - # @return int - # @since 1.2 - # - def GetStringWidth(s) - #Get width of a string in the current font - s = s.to_s; - cw = @current_font['cw'] - w = 0; - if (@is_unicode) - unicode = UTF8StringToArray(s); - unicode.each do |char| - if (!cw[char].nil?) - w += cw[char]; - # This should not happen. UTF8StringToArray should guarentee the array is ascii values. - # elsif (c!cw[char[0]].nil?) - # w += cw[char[0]]; - # elsif (!cw[char.chr].nil?) - # w += cw[char.chr]; - elsif (!@current_font['desc']['MissingWidth'].nil?) - w += @current_font['desc']['MissingWidth']; # set default size - else - w += 500; - end - end - else - s.each_byte do |c| - if cw[c.chr] - w += cw[c.chr]; - elsif cw[?c.chr] - w += cw[?c.chr] - end - end - end - return (w * @font_size / 1000.0); - end - alias_method :get_string_width, :GetStringWidth - - # - # Defines the line width. By default, the value equals 0.2 mm. The method can be called before the first page is created and the value is retained from page to page. - # @param float :width The width. - # @since 1.0 - # @see Line(), Rect(), Cell(), MultiCell() - # - def SetLineWidth(width) - #Set line width - @line_width = width; - if (@page>0) - out(sprintf('%.2f w', width*@k)); - end - end - alias_method :set_line_width, :SetLineWidth - - # - # Draws a line between two points. - # @param float :x1 Abscissa of first point - # @param float :y1 Ordinate of first point - # @param float :x2 Abscissa of second point - # @param float :y2 Ordinate of second point - # @since 1.0 - # @see SetLineWidth(), SetDrawColor() - # - def Line(x1, y1, x2, y2) - #Draw a line - out(sprintf('%.2f %.2f m %.2f %.2f l S', x1 * @k, (@h - y1) * @k, x2 * @k, (@h - y2) * @k)); - end - alias_method :line, :Line - - def Circle(mid_x, mid_y, radius, style='') - mid_y = (@h-mid_y)*@k - out(sprintf("q\n")) # postscript content in pdf - # init line type etc. with /GSD gs G g (grey) RG rg (RGB) w=line witdh etc. - out(sprintf("1 j\n")) # line join - # translate ("move") circle to mid_y, mid_y - out(sprintf("1 0 0 1 %f %f cm", mid_x, mid_y)) - kappa = 0.5522847498307933984022516322796 - # Quadrant 1 - x_s = 0.0 # 12 o'clock - y_s = 0.0 + radius - x_e = 0.0 + radius # 3 o'clock - y_e = 0.0 - out(sprintf("%f %f m\n", x_s, y_s)) # move to 12 o'clock - # cubic bezier control point 1, start height and kappa * radius to the right - bx_e1 = x_s + (radius * kappa) - by_e1 = y_s - # cubic bezier control point 2, end and kappa * radius above - bx_e2 = x_e - by_e2 = y_e + (radius * kappa) - # draw cubic bezier from current point to x_e/y_e with bx_e1/by_e1 and bx_e2/by_e2 as bezier control points - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - # Quadrant 2 - x_s = x_e - y_s = y_e # 3 o'clock - x_e = 0.0 - y_e = 0.0 - radius # 6 o'clock - bx_e1 = x_s # cubic bezier point 1 - by_e1 = y_s - (radius * kappa) - bx_e2 = x_e + (radius * kappa) # cubic bezier point 2 - by_e2 = y_e - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - # Quadrant 3 - x_s = x_e - y_s = y_e # 6 o'clock - x_e = 0.0 - radius - y_e = 0.0 # 9 o'clock - bx_e1 = x_s - (radius * kappa) # cubic bezier point 1 - by_e1 = y_s - bx_e2 = x_e # cubic bezier point 2 - by_e2 = y_e - (radius * kappa) - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - # Quadrant 4 - x_s = x_e - y_s = y_e # 9 o'clock - x_e = 0.0 - y_e = 0.0 + radius # 12 o'clock - bx_e1 = x_s # cubic bezier point 1 - by_e1 = y_s + (radius * kappa) - bx_e2 = x_e - (radius * kappa) # cubic bezier point 2 - by_e2 = y_e - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - if style=='F' - op='f' - elsif style=='FD' or style=='DF' - op='b' - else - op='s' - end - out(sprintf("#{op}\n")) # stroke circle, do not fill and close path - # for filling etc. b, b*, f, f* - out(sprintf("Q\n")) # finish postscript in PDF - end - alias_method :circle, :Circle - - # - # Outputs a rectangle. It can be drawn (border only), filled (with no border) or both. - # @param float :x Abscissa of upper-left corner - # @param float :y Ordinate of upper-left corner - # @param float :w Width - # @param float :h Height - # @param string :style Style of rendering. Possible values are:
    • D or empty string: draw (default)
    • F: fill
    • DF or FD: draw and fill
    - # @since 1.0 - # @see SetLineWidth(), SetDrawColor(), SetFillColor() - # - def Rect(x, y, w, h, style='') - #Draw a rectangle - if (style=='F') - op='f'; - elsif (style=='FD' or style=='DF') - op='B'; - else - op='S'; - end - out(sprintf('%.2f %.2f %.2f %.2f re %s', x * @k, (@h - y) * @k, w * @k, -h * @k, op)); - end - alias_method :rect, :Rect - - # - # Imports a TrueType or Type1 font and makes it available. It is necessary to generate a font definition file first with the makefont.rb utility. The definition file (and the font file itself when embedding) must be present either in the current directory or in the one indicated by FPDF_FONTPATH if the constant is defined. If it could not be found, the error "Could not include font definition file" is generated. - # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02]. - # Example:
    - #
    -	# :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):
    • empty string: regular (default)
    • B: bold
    • I: italic
    • BI or IB: bold italic
    - # @param string :file The font definition file. By default, the name is built from the family and style, in lower case with no space. - # @since 1.5 - # @see SetFont() - # - def AddFont(family, style='', file='') - if (family.empty?) - return; - end - - #Add a TrueType or Type1 font - family = family.downcase - if ((!@is_unicode) and (family == 'arial')) - family = 'helvetica'; - end - - style=style.upcase - style=style.gsub('U',''); - style=style.gsub('D',''); - if (style == 'IB') - style = 'BI'; - end - - fontkey = family + style; - # check if the font has been already added - if !@fonts[fontkey].nil? - return; - end - - if (file=='') - file = family.gsub(' ', '') + style.downcase + '.rb'; - end - font_file_name = getfontpath(file) - if (font_file_name.nil?) - # try to load the basic file without styles - file = family.gsub(' ', '') + '.rb'; - font_file_name = getfontpath(file) - end - if font_file_name.nil? - Error("Could not find font #{file}.") - end - require(getfontpath(file)) - font_desc = TCPDFFontDescriptor.font(file) - - if (font_desc[:name].nil? and @@fpdf_charwidths.nil?) - Error('Could not include font definition file'); - end - - i = @fonts.length+1; - if (@is_unicode) - @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg], 'cMap' => font_desc[:cMap], 'registry' => font_desc[:registry]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - else - @fonts[fontkey]={'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - end - - if (!font_desc[:diff].nil? and (!font_desc[:diff].empty?)) - #Search existing encodings - d=0; - nb=@diffs.length; - 1.upto(nb) do |i| - if (@diffs[i]== font_desc[:diff]) - d = i; - break; - end - end - if (d==0) - d = nb+1; - @diffs[d] = font_desc[:diff]; - end - @fonts[fontkey]['diff'] = d; - end - if (font_desc[:file] and font_desc[:file].length > 0) - if (font_desc[:type] == "TrueType") or (font_desc[:type] == "TrueTypeUnicode") - @font_files[font_desc[:file]] = {'length1' => font_desc[:originalsize]} - else - @font_files[font_desc[:file]] = {'length1' => font_desc[:size1], 'length2' => font_desc[:size2]} - end - end - end - alias_method :add_font, :AddFont - - # - # Sets the font used to print character strings. It is mandatory to call this method at least once before printing text or the resulting document would not be valid. - # The font can be either a standard one or a font added via the AddFont() method. Standard fonts use Windows encoding cp1252 (Western Europe). - # The method can be called before the first page is created and the font is retained from page to page. - # If you just wish to change the current font size, it is simpler to call SetFontSize(). - # Note: for the standard fonts, the font metric files must be accessible. There are three possibilities for this:
    • They are in the current directory (the one where the running script lies)
    • They are in one of the directories defined by the include_path parameter
    • They are in the directory defined by the FPDF_FONTPATH constant

    - # Example for the last case (note the trailing slash):
    - #
    -	# 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');
    -	# 

    - # If the file corresponding to the requested font is not found, the error "Could not include font metric file" is generated. - # @param string :family Family font. It can be either a name defined by AddFont() or one of the standard families (case insensitive):
    • Courier (fixed-width)
    • Helvetica or Arial (synonymous; sans serif)
    • Times (serif)
    • Symbol (symbolic)
    • ZapfDingbats (symbolic)
    It is also possible to pass an empty string. In that case, the current family is retained. - # @param string :style Font style. Possible values are (case insensitive):
    • empty string: regular
    • B: bold
    • I: italic
    • U: underline
    or any combination. The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats - # @param float :size Font size in points. The default value is the current size. If no size has been specified since the beginning of the document, the value taken is 12 - # @since 1.0 - # @see AddFont(), SetFontSize(), Cell(), MultiCell(), Write() - # - def SetFont(family, style='', size=0) - # save previous values - @prevfont_family = @font_family; - @prevfont_style = @font_style; - - family=family.downcase; - if (family=='') - family=@font_family; - end - if ((!@is_unicode) and (family == 'arial')) - family = 'helvetica'; - elsif ((family=="symbol") or (family=="zapfdingbats")) - style=''; - end - - style=style.upcase; - - if (style.include?('U')) - @underline=true; - style= style.gsub('U',''); - else - @underline=false; - end - if (style.include?('D')) - @deleted=true; - style= style.gsub('D',''); - else - @deleted=false; - end - if (style=='IB') - style='BI'; - end - if (size==0) - size=@font_size_pt; - end - - # try to add font (if not already added) - AddFont(family, style); - - #Test if font is already selected - if ((@font_family == family) and (@font_style == style) and (@font_size_pt == size)) - return; - end - - fontkey = family + style; - style = '' if (@fonts[fontkey].nil? and !@fonts[family].nil?) - - #Test if used for the first time - if (@fonts[fontkey].nil?) - #Check if one of the standard fonts - if (!@core_fonts[fontkey].nil?) - if @@fpdf_charwidths[fontkey].nil? - #Load metric file - file = family; - if ((family!='symbol') and (family!='zapfdingbats')) - file += style.downcase; - end - if (getfontpath(file + '.rb').nil?) - # try to load the basic file without styles - file = family; - fontkey = family; - end - require(getfontpath(file + '.rb')); - font_desc = TCPDFFontDescriptor.font(file) - if ((@is_unicode and ctg.nil?) or ((!@is_unicode) and (@@fpdf_charwidths[fontkey].nil?)) ) - Error("Could not include font metric file [" + fontkey + "]: " + getfontpath(file + ".rb")); - end - end - i = @fonts.length + 1; - - if (@is_unicode) - @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - else - @fonts[fontkey] = {'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - end - else - Error('Undefined font: ' + family + ' ' + style); - end - end - #Select it - @font_family = family; - @font_style = style; - @font_size_pt = size; - @font_size = size / @k; - @current_font = @fonts[fontkey]; # was & may need deep copy? - if (@page>0) - out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); - end - end - alias_method :set_font, :SetFont - - # - # Defines the size of the current font. - # @param float :size The size (in points) - # @since 1.0 - # @see SetFont() - # - def SetFontSize(size) - #Set font size in points - if (@font_size_pt== size) - return; - end - @font_size_pt = size; - @font_size = size.to_f / @k; - if (@page > 0) - out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); - end - end - alias_method :set_font_size, :SetFontSize - - # - # Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document.
    - # The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is defined with SetLink(). - # @since 1.5 - # @see Cell(), Write(), Image(), Link(), SetLink() - # - def AddLink() - #Create a new internal link - n=@links.length+1; - @links[n]=[0,0]; - return n; - end - alias_method :add_link, :AddLink - - # - # Defines the page and position a link points to - # @param int :link The link identifier returned by AddLink() - # @param float :y Ordinate of target position; -1 indicates the current position. The default value is 0 (top of page) - # @param int :page Number of target page; -1 indicates the current page. This is the default value - # @since 1.5 - # @see AddLink() - # - def SetLink(link, y=0, page=-1) - #Set destination of internal link - if (y==-1) - y=@y; - end - if (page==-1) - page=@page; - end - @links[link] = [page, y] - end - alias_method :set_link, :SetLink - - # - # Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), Write() or Image(), but this method can be useful for instance to define a clickable area inside an image. - # @param float :x Abscissa of the upper-left corner of the rectangle - # @param float :y Ordinate of the upper-left corner of the rectangle - # @param float :w Width of the rectangle - # @param float :h Height of the rectangle - # @param mixed :link URL or identifier returned by AddLink() - # @since 1.5 - # @see AddLink(), Cell(), Write(), Image() - # - def Link(x, y, w, h, link) - #Put a link on the page - @page_links ||= Array.new - @page_links[@page] ||= Array.new - @page_links[@page].push([x * @k, @h_pt - y * @k, w * @k, h*@k, link]); - end - alias_method :link, :Link - - # - # Prints a character string. The origin is on the left of the first charcter, on the baseline. This method allows to place a string precisely on the page, but it is usually easier to use Cell(), MultiCell() or Write() which are the standard methods to print text. - # @param float :x Abscissa of the origin - # @param float :y Ordinate of the origin - # @param string :txt String to print - # @since 1.0 - # @see SetFont(), SetTextColor(), Cell(), MultiCell(), Write() - # - def Text(x, y, txt) - #Output a string - s=sprintf('BT %.2f %.2f Td (%s) Tj ET', x * @k, (@h-y) * @k, escapetext(txt)); - if (@underline and (txt!='')) - s += ' ' + dolinetxt(x, y, txt); - end - if (@color_flag) - s='q ' + @text_color + ' ' + s + ' Q'; - end - out(s); - end - alias_method :text, :Text - - # - # Whenever a page break condition is met, the method is called, and the break is issued or not depending on the returned value. The default implementation returns a value according to the mode selected by SetAutoPageBreak().
    - # This method is called automatically and should not be called directly by the application.
    - # Example:
    - # The method is overriden in an inherited class in order to obtain a 3 column layout:
    - #
    -	# 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.
    - # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. - # @param float :w Cell width. If 0, the cell extends up to the right margin. - # @param float :h Cell height. Default value: 0. - # @param string :txt String to print. Default value: empty string. - # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    - # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line
    • 2: below
    - # Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. - # @param string :align Allows to center or align the text. Possible values are:
    • L or empty string: left align (default value)
    • C: center
    • R: right align
    - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @param mixed :link URL or identifier returned by AddLink(). - # @since 1.0 - # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak() - # - def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil) - #Output a cell - k=@k; - if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) - #Automatic page break - if @pages[@page+1].nil? - x = @x; - ws = @ws; - if (ws > 0) - @ws = 0; - out('0 Tw'); - end - AddPage(@cur_orientation); - @x = x; - if (ws > 0) - @ws = ws; - out(sprintf('%.3f Tw', ws * k)); - end - else - @page += 1; - @y=@t_margin; - end - end - - if (w == 0) - w = @w - @r_margin - @x; - end - s = ''; - if ((fill.to_i == 1) or (border.to_i == 1)) - if (fill.to_i == 1) - op = (border.to_i == 1) ? 'B' : 'f'; - else - op = 'S'; - end - s = sprintf('%.2f %.2f %.2f %.2f re %s ', @x * k, (@h - @y) * k, w * k, -h * k, op); - end - if (border.is_a?(String)) - x=@x; - y=@y; - if (border.include?('L')) - s<0) - # Go to next line - @y += h; - if (ln == 1) - @x = @l_margin; - end - else - @x += w; - end - end - alias_method :cell, :Cell - - # - # This method allows printing text with line breaks. They can be automatic (as soon as the text reaches the right border of the cell) or explicit (via the \n character). As many cells as necessary are output, one below the other.
    - # Text can be aligned, centered or justified. The cell block can be framed and the background painted. - # @param float :w Width of cells. If 0, they extend up to the right margin of the page. - # @param float :h Height of cells. - # @param string :txt String to print - # @param mixed :border Indicates if borders must be drawn around the cell block. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    - # @param string :align Allows to center or align the text. Possible values are:
    • L or empty string: left align
    • C: center
    • R: right align
    • J: justification (default value)
    - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line [DEFAULT]
    • 2: below
    - # @since 1.3 - # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), Cell(), Write(), SetAutoPageBreak() - # - def MultiCell(w, h, txt, border=0, align='J', fill=0, ln=1) - - # save current position - prevx = @x; - prevy = @y; - prevpage = @page; - - #Output text with automatic or explicit line breaks - - if (w == 0) - w = @w - @r_margin - @x; - end - - wmax = (w - 3 * @c_margin); - - s = txt.gsub("\r", ''); # remove carriage returns - nb = s.length; - - b=0; - if (border) - if (border==1) - border='LTRB'; - b='LRT'; - b2='LR'; - elsif border.is_a?(String) - b2=''; - if (border.include?('L')) - b2<<'L'; - end - if (border.include?('R')) - b2<<'R'; - end - b=(border.include?('T')) ? b2 + 'T' : b2; - end - end - sep=-1; - to_index=0; - from_j=0; - l=0; - ns=0; - nl=1; - - while to_index < nb - #Get next character - c = s[to_index]; - if c == "\n"[0] - #Explicit line break - if @ws > 0 - @ws = 0 - out('0 Tw') - end - #Ed Moss - change begin - end_i = to_index == 0 ? 0 : to_index - 1 - # Changed from s[from_j..to_index] to fix bug reported by Hans Allis. - from_j = to_index == 0 ? 1 : from_j - Cell(w, h, s[from_j..end_i], b, 2, align, fill) - #change end - to_index += 1 - sep=-1 - from_j=to_index - l=0 - ns=0 - nl += 1 - b = b2 if border and nl==2 - next - end - if (c == " "[0]) - sep = to_index; - ls = l; - ns += 1; - end - - l = GetStringWidth(s[from_j, to_index - from_j]); - - if (l > wmax) - #Automatic line break - if (sep == -1) - if (to_index == from_j) - to_index += 1; - end - if (@ws > 0) - @ws = 0; - out('0 Tw'); - end - Cell(w, h, s[from_j..to_index-1], b, 2, align, fill) # my FPDF version - else - if (align=='J' || align=='justify' || align=='justified') - @ws = (ns>1) ? (wmax-ls)/(ns-1) : 0; - out(sprintf('%.3f Tw', @ws * @k)); - end - Cell(w, h, s[from_j..sep], b, 2, align, fill); - to_index = sep + 1; - end - sep=-1; - from_j = to_index; - l=0; - ns=0; - nl += 1; - if (border and (nl==2)) - b = b2; - end - else - to_index += 1; - end - end - #Last chunk - if (@ws>0) - @ws=0; - out('0 Tw'); - end - if (border.is_a?(String) and border.include?('B')) - b<<'B'; - end - Cell(w, h, s[from_j, to_index-from_j], b, 2, align, fill); - - # move cursor to specified position - # since 2007-03-03 - if (ln == 1) - # go to the beginning of the next line - @x = @l_margin; - elsif (ln == 0) - # go to the top-right of the cell - @page = prevpage; - @y = prevy; - @x = prevx + w; - elsif (ln == 2) - # go to the bottom-left of the cell - @x = prevx; - end - end - alias_method :multi_cell, :MultiCell - - # - # This method prints text from the current position. When the right margin is reached (or the \n character is met) a line break occurs and text continues from the left margin. Upon method exit, the current position is left just at the end of the text. It is possible to put a link on the text.
    - # Example:
    - #
    -	# #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 wmax) - #Automatic line break (word wrapping) - if (sep == -1) - if (@x > @l_margin) - #Move to next line - @x = @l_margin; - @y += h; - w=@w - @r_margin - @x; - wmax=(w - 3 * @c_margin); - i += 1 - nl += 1 - next - end - if (i == j) - i += 1 - end - Cell(w, h, s[j, (i-1)], 0, 2, '', fill, link); - else - Cell(w, h, s[j, (sep-j)], 0, 2, '', fill, link); - i = sep+1; - end - sep = -1; - j = i; - l = 0; - if (nl==1) - @x = @l_margin; - w = @w - @r_margin - @x; - wmax = (w - 3 * @c_margin); - end - nl += 1; - else - i += 1; - end - end - #Last chunk - if (i != j) - Cell(GetStringWidth(s[j..i]), h, s[j..i], 0, 0, '', fill, link); - end - end - alias_method :write, :Write - - # - # Puts an image in the page. The upper-left corner must be given. The dimensions can be specified in different ways:
    • explicit width and height (expressed in user unit)
    • one explicit dimension, the other being calculated automatically in order to keep the original proportions
    • no explicit dimension, in which case the image is put at 72 dpi
    - # Supported formats are JPEG and PNG. - # For JPEG, all flavors are allowed:
    • gray scales
    • true colors (24 bits)
    • CMYK (32 bits)
    - # For PNG, are allowed:
    • gray scales on at most 8 bits (256 levels)
    • indexed colors
    • true colors (24 bits)
    - # but are not supported:
    • Interlacing
    • Alpha channel
    - # If a transparent color is defined, it will be taken into account (but will be only interpreted by Acrobat 4 and above).
    - # The format can be specified explicitly or inferred from the file extension.
    - # It is possible to put a link on the image.
    - # Remark: if an image is used several times, only one copy will be embedded in the file.
    - # @param string :file Name of the file containing the image. - # @param float :x Abscissa of the upper-left corner. - # @param float :y Ordinate of the upper-left corner. - # @param float :w Width of the image in the page. If not specified or equal to zero, it is automatically calculated. - # @param float :h Height of the image in the page. If not specified or equal to zero, it is automatically calculated. - # @param string :type Image format. Possible values are (case insensitive): JPG, JPEG, PNG. If not specified, the type is inferred from the file extension. - # @param mixed :link URL or identifier returned by AddLink(). - # @since 1.1 - # @see AddLink() - # - def Image(file, x, y, w=0, h=0, type='', link=nil) - #Put an image on the page - if (@images[file].nil?) - #First use of image, get info - if (type == '') - pos = File::basename(file).rindex('.'); - if (pos.nil? or pos == 0) - Error('Image file has no extension and no type was specified: ' + file); - end - pos = file.rindex('.'); - type = file[pos+1..-1]; - end - type.downcase! - if (type == 'jpg' or type == 'jpeg') - info=parsejpg(file); - elsif (type == 'png') - info=parsepng(file); - elsif (type == 'gif') - tmpFile = imageToPNG(file); - info=parsepng(tmpFile.path); - tmpFile.delete - else - #Allow for additional formats - mtd='parse' + type; - if (!self.respond_to?(mtd)) - Error('Unsupported image type: ' + type); - end - info=send(mtd, file); - end - info['i']=@images.length+1; - @images[file] = info; - else - info=@images[file]; - end - #Automatic width and height calculation if needed - if ((w == 0) and (h == 0)) - rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k)) - rescale_x = 1 if rescale_x >= 1 - if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak()) - #Automatic page break - if @pages[@page+1].nil? - ws = @ws; - if (ws > 0) - @ws = 0; - out('0 Tw'); - end - AddPage(@cur_orientation); - if (ws > 0) - @ws = ws; - out(sprintf('%.3f Tw', ws * @k)); - end - else - @page += 1; - end - y=@t_margin; - end - rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k)) - rescale_y = 1 if rescale_y >= 1 - rescale = rescale_y >= rescale_x ? rescale_x : rescale_y - - #Put image at 72 dpi - # 2004-06-14 :: Nicola Asuni, scale factor where added - w = info['w'] * rescale / (@img_scale * @k); - h = info['h'] * rescale / (@img_scale * @k); - elsif (w == 0) - w = h * info['w'] / info['h']; - elsif (h == 0) - h = w * info['h'] / info['w']; - end - out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i'])); - if (link) - Link(x, y, w, h, link); - end - - #2002-07-31 - Nicola Asuni - # set right-bottom corner coordinates - @img_rb_x = x + w; - @img_rb_y = y + h; - end - alias_method :image, :Image - - # - # Performs a line break. The current abscissa goes back to the left margin and the ordinate increases by the amount passed in parameter. - # @param float :h The height of the break. By default, the value equals the height of the last printed cell. - # @since 1.0 - # @see Cell() - # - def Ln(h='') - #Line feed; default value is last cell height - @x=@l_margin; - if (h.is_a?(String)) - @y += @lasth; - else - @y += h; - end - - k=@k; - if (@y > @page_break_trigger and !@in_footer and AcceptPageBreak()) - #Automatic page break - if @pages[@page+1].nil? - x = @x; - ws = @ws; - if (ws > 0) - @ws = 0; - out('0 Tw'); - end - AddPage(@cur_orientation); - @x = x; - if (ws > 0) - @ws = ws; - out(sprintf('%.3f Tw', ws * k)); - end - else - @page += 1; - @y=@t_margin; - end - end - - end - alias_method :ln, :Ln - - # - # Returns the abscissa of the current position. - # @return float - # @since 1.2 - # @see SetX(), GetY(), SetY() - # - def GetX() - #Get x position - return @x; - end - alias_method :get_x, :GetX - - # - # Defines the abscissa of the current position. If the passed value is negative, it is relative to the right of the page. - # @param float :x The value of the abscissa. - # @since 1.2 - # @see GetX(), GetY(), SetY(), SetXY() - # - def SetX(x) - #Set x position - if (x>=0) - @x = x; - else - @x=@w+x; - end - end - alias_method :set_x, :SetX - - # - # Returns the ordinate of the current position. - # @return float - # @since 1.0 - # @see SetY(), GetX(), SetX() - # - def GetY() - #Get y position - return @y; - end - alias_method :get_y, :GetY - - # - # Moves the current abscissa back to the left margin and sets the ordinate. If the passed value is negative, it is relative to the bottom of the page. - # @param float :y The value of the ordinate. - # @since 1.0 - # @see GetX(), GetY(), SetY(), SetXY() - # - def SetY(y) - #Set y position and reset x - @x=@l_margin; - if (y>=0) - @y = y; - else - @y=@h+y; - end - end - alias_method :set_y, :SetY - - # - # Defines the abscissa and ordinate of the current position. If the passed values are negative, they are relative respectively to the right and bottom of the page. - # @param float :x The value of the abscissa - # @param float :y The value of the ordinate - # @since 1.2 - # @see SetX(), SetY() - # - def SetXY(x, y) - #Set x and y positions - SetY(y); - SetX(x); - end - alias_method :set_xy, :SetXY - - # - # Send the document to a given destination: string, local file or browser. In the last case, the plug-in may be used (if present) or a download ("Save as" dialog box) may be forced.
    - # The method first calls Close() if necessary to terminate the document. - # @param string :name The name of the file. If not given, the document will be sent to the browser (destination I) with the name doc.pdf. - # @param string :dest Destination where to send the document. It can take one of the following values:
    • I: send the file inline to the browser. The plug-in is used if available. The name given by name is used when one selects the "Save as" option on the link generating the PDF.
    • D: send to the browser and force a file download with the name given by name.
    • F: save to a local file with the name given by name.
    • S: return the document as a string. name is ignored.
    If the parameter is not specified but a name is given, destination is F. If no parameter is specified at all, destination is I.
    - # @since 1.0 - # @see Close() - # - def Output(name='', dest='') - #Output PDF to some destination - #Finish document if necessary - if (@state < 3) - Close(); - end - #Normalize parameters - # Boolean no longer supported - # if (dest.is_a?(Boolean)) - # dest = dest ? 'D' : 'F'; - # end - dest = dest.upcase - if (dest=='') - if (name=='') - name='doc.pdf'; - dest='I'; - else - dest='F'; - end - end - case (dest) - when 'I' - # This is PHP specific code - ##Send to standard output - # if (ob_get_contents()) - # Error('Some data has already been output, can\'t send PDF file'); - # end - # if (php_sapi_name()!='cli') - # #We send to a browser - # header('Content-Type: application/pdf'); - # if (headers_sent()) - # Error('Some data has already been output to browser, can\'t send PDF file'); - # end - # header('Content-Length: ' + @buffer.length); - # header('Content-disposition: inline; filename="' + name + '"'); - # end - return @buffer; - - when 'D' - # PHP specific - #Download file - # if (ob_get_contents()) - # Error('Some data has already been output, can\'t send PDF file'); - # end - # if (!_SERVER['HTTP_USER_AGENT'].nil? && SERVER['HTTP_USER_AGENT'].include?('MSIE')) - # header('Content-Type: application/force-download'); - # else - # header('Content-Type: application/octet-stream'); - # end - # if (headers_sent()) - # Error('Some data has already been output to browser, can\'t send PDF file'); - # end - # header('Content-Length: '+ @buffer.length); - # header('Content-disposition: attachment; filename="' + name + '"'); - return @buffer; - - when 'F' - open(name,'wb') do |f| - f.write(@buffer) - end - # PHP code - # #Save to local file - # f=open(name,'wb'); - # if (!f) - # Error('Unable to create output file: ' + name); - # end - # fwrite(f,@buffer,@buffer.length); - # f.close - - when 'S' - #Return as a string - return @buffer; - else - Error('Incorrect output destination: ' + dest); - - end - return ''; - end - alias_method :output, :Output - - # Protected methods - - # - # Check for locale-related bug - # @access protected - # - def dochecks() - #Check for locale-related bug - if (1.1==1) - Error('Don\'t alter the locale before including class file'); - end - #Check for decimal separator - if (sprintf('%.1f',1.0)!='1.0') - setlocale(LC_NUMERIC,'C'); - end - end - - # - # Return fonts path - # @access protected - # - def getfontpath(file) - # Is it in the @@font_path? - if @@font_path - fpath = File.join @@font_path, file - if File.exists?(fpath) - return fpath - end - end - # Is it in this plugin's font folder? - fpath = File.join File.dirname(__FILE__), 'fonts', file - if File.exists?(fpath) - return fpath - end - # Could not find it. - nil - end - - # - # Start document - # @access protected - # - def begindoc() - #Start document - @state=1; - out('%PDF-1.3'); - end - - # - # putpages - # @access protected - # - def putpages() - nb = @page; - if (@alias_nb_pages) - nbstr = UTF8ToUTF16BE(nb.to_s, false); - #Replace number of pages - 1.upto(nb) do |n| - @pages[n].gsub!(@alias_nb_pages, nbstr) - end - end - if @def_orientation=='P' - w_pt=@fw_pt - h_pt=@fh_pt - else - w_pt=@fh_pt - h_pt=@fw_pt - end - filter=(@compress) ? '/Filter /FlateDecode ' : '' - 1.upto(nb) do |n| - #Page - newobj - out('<>>>'; - else - l=@links[pl[4]]; - h=!@orientation_changes[l[0]].nil? ? w_pt : h_pt; - annots<>',1+2*l[0], h-l[1]*@k); - end - end - out(annots + ']'); - end - out('/Contents ' + (@n+1).to_s + ' 0 R>>'); - out('endobj'); - #Page content - p=(@compress) ? gzcompress(@pages[n]) : @pages[n]; - newobj(); - out('<<' + filter + '/Length '+ p.length.to_s + '>>'); - putstream(p); - out('endobj'); - end - #Pages root - @offsets[1]=@buffer.length; - out('1 0 obj'); - out('<>'); - out('endobj'); - end - - # - # Adds fonts - # putfonts - # @access protected - # - def putfonts() - nf=@n; - @diffs.each do |diff| - #Encodings - newobj(); - out('<>'); - out('endobj'); - end - @font_files.each do |file, info| - #Font file embedding - newobj(); - @font_files[file]['n']=@n; - font=''; - open(getfontpath(file),'rb') do |f| - font = f.read(); - end - compressed=(file[-2,2]=='.z'); - if (!compressed && !info['length2'].nil?) - header=((font[0][0])==128); - if (header) - #Strip first binary header - font=font[6]; - end - if header && (font[info['length1']][0] == 128) - #Strip second binary header - font=font[0..info['length1']] + font[info['length1']+6]; - end - end - out('<>'); - open(getfontpath(file),'rb') do |f| - putstream(font) - end - out('endobj'); - end - @fonts.each do |k, font| - #Font objects - @fonts[k]['n']=@n+1; - type = font['type']; - name = font['name']; - if (type=='core') - #Standard font - newobj(); - out('<>'); - out('endobj'); - elsif type == 'Type0' - putType0(font) - elsif (type=='Type1' || type=='TrueType') - #Additional Type1 or TrueType font - newobj(); - out('<>'); - out('endobj'); - #Widths - newobj(); - cw=font['cw']; # & - s='['; - 32.upto(255) do |i| - s << cw[i.chr] + ' '; - end - out(s + ']'); - out('endobj'); - #Descriptor - newobj(); - s='<>'); - out('endobj'); - else - #Allow for additional types - mtd='put' + type.downcase; - if (!self.respond_to?(mtd)) - Error('Unsupported font type: ' + type) - else - self.send(mtd,font) - end - end - end - end - - def putType0(font) - #Type0 - newobj(); - out('<>') - out('endobj') - #CIDFont - newobj() - out('<>') - out('/FontDescriptor '+(@n+1).to_s+' 0 R') - w='/W [1 [' - font['cw'].keys.sort.each {|key| - w+=font['cw'][key].to_s + " " -# ActionController::Base::logger.debug key.to_s -# ActionController::Base::logger.debug font['cw'][key].to_s - } - out(w+'] 231 325 500 631 [500] 326 389 500]') - out('>>') - out('endobj') - #Font descriptor - newobj() - out('<>') - out('endobj') - end - - # - # putimages - # @access protected - # - def putimages() - filter=(@compress) ? '/Filter /FlateDecode ' : ''; - @images.each do |file, info| # was while(list(file, info)=each(@images)) - newobj(); - @images[file]['n']=@n; - out('<>'); - putstream(info['data']); - @images[file]['data']=nil - out('endobj'); - #Palette - if (info['cs']=='Indexed') - newobj(); - pal=(@compress) ? gzcompress(info['pal']) : info['pal']; - out('<<' + filter + '/Length ' + pal.length.to_s + '>>'); - putstream(pal); - out('endobj'); - end - end - end - - # - # putxobjectdict - # @access protected - # - def putxobjectdict() - @images.each_value do |image| - out('/I' + image['i'].to_s + ' ' + image['n'].to_s + ' 0 R'); - end - end - - # - # putresourcedict - # @access protected - # - def putresourcedict() - out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); - out('/Font <<'); - @fonts.each_value do |font| - out('/F' + font['i'].to_s + ' ' + font['n'].to_s + ' 0 R'); - end - out('>>'); - out('/XObject <<'); - putxobjectdict(); - out('>>'); - end - - # - # putresources - # @access protected - # - def putresources() - putfonts(); - putimages(); - #Resource dictionary - @offsets[2]=@buffer.length; - out('2 0 obj'); - out('<<'); - putresourcedict(); - out('>>'); - out('endobj'); - end - - # - # putinfo - # @access protected - # - def putinfo() - out('/Producer ' + textstring(PDF_PRODUCER)); - if (!@title.nil?) - out('/Title ' + textstring(@title)); - end - if (!@subject.nil?) - out('/Subject ' + textstring(@subject)); - end - if (!@author.nil?) - out('/Author ' + textstring(@author)); - end - if (!@keywords.nil?) - out('/Keywords ' + textstring(@keywords)); - end - if (!@creator.nil?) - out('/Creator ' + textstring(@creator)); - end - out('/CreationDate ' + textstring('D:' + Time.now.strftime('%Y%m%d%H%M%S'))); - end - - # - # putcatalog - # @access protected - # - def putcatalog() - out('/Type /Catalog'); - out('/Pages 1 0 R'); - if (@zoom_mode=='fullpage') - out('/OpenAction [3 0 R /Fit]'); - elsif (@zoom_mode=='fullwidth') - out('/OpenAction [3 0 R /FitH null]'); - elsif (@zoom_mode=='real') - out('/OpenAction [3 0 R /XYZ null null 1]'); - elsif (!@zoom_mode.is_a?(String)) - out('/OpenAction [3 0 R /XYZ null null ' + (@zoom_mode/100) + ']'); - end - if (@layout_mode=='single') - out('/PageLayout /SinglePage'); - elsif (@layout_mode=='continuous') - out('/PageLayout /OneColumn'); - elsif (@layout_mode=='two') - out('/PageLayout /TwoColumnLeft'); - end - end - - # - # puttrailer - # @access protected - # - def puttrailer() - out('/Size ' + (@n+1).to_s); - out('/Root ' + @n.to_s + ' 0 R'); - out('/Info ' + (@n-1).to_s + ' 0 R'); - end - - # - # putheader - # @access protected - # - def putheader() - out('%PDF-' + @pdf_version); - end - - # - # enddoc - # @access protected - # - def enddoc() - putheader(); - putpages(); - putresources(); - #Info - newobj(); - out('<<'); - putinfo(); - out('>>'); - out('endobj'); - #Catalog - newobj(); - out('<<'); - putcatalog(); - out('>>'); - out('endobj'); - #Cross-ref - o=@buffer.length; - out('xref'); - out('0 ' + (@n+1).to_s); - out('0000000000 65535 f '); - 1.upto(@n) do |i| - out(sprintf('%010d 00000 n ',@offsets[i])); - end - #Trailer - out('trailer'); - out('<<'); - puttrailer(); - out('>>'); - out('startxref'); - out(o); - out('%%EOF'); - @state=3; - end - - # - # beginpage - # @access protected - # - def beginpage(orientation) - @page += 1; - @pages[@page]=''; - @state=2; - @x=@l_margin; - @y=@t_margin; - @font_family=''; - #Page orientation - if (orientation.empty?) - orientation=@def_orientation; - else - orientation.upcase! - if (orientation!=@def_orientation) - @orientation_changes[@page]=true; - end - end - if (orientation!=@cur_orientation) - #Change orientation - if (orientation=='P') - @w_pt=@fw_pt; - @h_pt=@fh_pt; - @w=@fw; - @h=@fh; - else - @w_pt=@fh_pt; - @h_pt=@fw_pt; - @w=@fh; - @h=@fw; - end - @page_break_trigger=@h-@b_margin; - @cur_orientation = orientation; - end - end - - # - # End of page contents - # @access protected - # - def endpage() - @state=1; - end - - # - # Begin a new object - # @access protected - # - def newobj() - @n += 1; - @offsets[@n]=@buffer.length; - out(@n.to_s + ' 0 obj'); - end - - # - # Underline and Deleted text - # @access protected - # - def dolinetxt(x, y, txt) - up = @current_font['up']; - ut = @current_font['ut']; - w = GetStringWidth(txt) + @ws * txt.count(' '); - sprintf('%.2f %.2f %.2f %.2f re f', x * @k, (@h - (y - up / 1000.0 * @font_size)) * @k, w * @k, -ut / 1000.0 * @font_size_pt); - end - - # - # Extract info from a JPEG file - # @access protected - # - def parsejpg(file) - a=getimagesize(file); - if (a.empty?) - Error('Missing or incorrect image file: ' + file); - end - if (!a[2].nil? and a[2]!='JPEG') - Error('Not a JPEG file: ' + file); - end - if (a['channels'].nil? or a['channels']==3) - colspace='DeviceRGB'; - elsif (a['channels']==4) - colspace='DeviceCMYK'; - else - colspace='DeviceGray'; - end - bpc=!a['bits'].nil? ? a['bits'] : 8; - #Read whole file - data=''; - - open(file,'rb') do |f| - data< a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data} - end - - def imageToPNG(file) - return unless Object.const_defined?(:Magick) - - img = Magick::ImageList.new(file) - img.format = 'PNG' # convert to PNG from gif - img.opacity = 0 # PNG alpha channel delete - - #use a temporary file.... - tmpFile = Tempfile.new(['', '_' + File::basename(file) + '.png'], @@k_path_cache); - tmpFile.binmode - tmpFile.print img.to_blob - tmpFile - ensure - tmpFile.close - end - - # - # Extract info from a PNG file - # @access protected - # - def parsepng(file) - f=open(file,'rb'); - #Check signature - if (f.read(8)!=137.chr + 'PNG' + 13.chr + 10.chr + 26.chr + 10.chr) - Error('Not a PNG file: ' + file); - end - #Read header chunk - f.read(4); - if (f.read(4)!='IHDR') - Error('Incorrect PNG file: ' + file); - end - w=freadint(f); - h=freadint(f); - bpc=f.read(1).unpack('C')[0]; - if (bpc>8) - Error('16-bit depth not supported: ' + file); - end - ct=f.read(1).unpack('C')[0]; - if (ct==0) - colspace='DeviceGray'; - elsif (ct==2) - colspace='DeviceRGB'; - elsif (ct==3) - colspace='Indexed'; - else - Error('Alpha channel not supported: ' + file); - end - if (f.read(1).unpack('C')[0] != 0) - Error('Unknown compression method: ' + file); - end - if (f.read(1).unpack('C')[0] != 0) - Error('Unknown filter method: ' + file); - end - if (f.read(1).unpack('C')[0] != 0) - Error('Interlacing not supported: ' + file); - end - f.read(4); - parms='/DecodeParms <>'; - #Scan chunks looking for palette, transparency and image data - pal=''; - trns=''; - data=''; - begin - n=freadint(f); - type=f.read(4); - if (type=='PLTE') - #Read palette - pal=f.read( n); - f.read(4); - elsif (type=='tRNS') - #Read transparency info - t=f.read( n); - if (ct==0) - trns = t[1].unpack('C')[0] - elsif (ct==2) - trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]] - else - pos=t.index(0.chr); - unless (pos.nil?) - trns = [pos] - end - end - f.read(4); - elsif (type=='IDAT') - #Read image data block - data< w, 'h' => h, 'cs' => colspace, 'bpc' => bpc, 'f'=>'FlateDecode', 'parms' => parms, 'pal' => pal, 'trns' => trns, 'data' => data} - ensure - f.close - end - - # - # Read a 4-byte integer from file - # @access protected - # - def freadint(f) - # Read a 4-byte integer from file - a = f.read(4).unpack('N') - return a[0] - end - - # - # Format a text string - # @access protected - # - def textstring(s) - if (@is_unicode) - #Convert string to UTF-16BE - s = UTF8ToUTF16BE(s, true); - end - return '(' + escape(s) + ')'; - end - - # - # Format a text string - # @access protected - # - def escapetext(s) - if (@is_unicode) - #Convert string to UTF-16BE - s = UTF8ToUTF16BE(s, false); - end - return escape(s); - end - - # - # Add \ before \, ( and ) - # @access protected - # - def escape(s) - # Add \ before \, ( and ) - s.gsub('\\','\\\\\\').gsub('(','\\(').gsub(')','\\)').gsub(13.chr, '\r') - end - - # - # - # @access protected - # - def putstream(s) - out('stream'); - out(s); - out('endstream'); - end - - # - # Add a line to the document - # @access protected - # - def out(s) - if (@state==2) - @pages[@page] << s.to_s + "\n"; - else - @buffer << s.to_s + "\n"; - end - end - - # - # Adds unicode fonts.
    - # Based on PDF Reference 1.3 (section 5) - # @access protected - # @author Nicola Asuni - # @since 1.52.0.TC005 (2005-01-05) - # - def puttruetypeunicode(font) - # Type0 Font - # A composite font composed of other fonts, organized hierarchically - newobj(); - out('<>'); - out('endobj'); - - # CIDFontType2 - # A CIDFont whose glyph descriptions are based on TrueType font technology - newobj(); - out('<>'); - out('endobj'); - - # ToUnicode - # is a stream object that contains the definition of the CMap - # (PDF Reference 1.3 chap. 5.9) - newobj(); - out('<>'); - out('stream'); - out('/CIDInit /ProcSet findresource begin'); - out('12 dict begin'); - out('begincmap'); - out('/CIDSystemInfo'); - out('<> def'); - out('/CMapName /Adobe-Identity-UCS def'); - out('/CMapType 2 def'); - out('1 begincodespacerange'); - out('<0000> '); - out('endcodespacerange'); - out('1 beginbfrange'); - out('<0000> <0000>'); - out('endbfrange'); - out('endcmap'); - out('CMapName currentdict /CMap defineresource pop'); - out('end'); - out('end'); - out('endstream'); - out('endobj'); - - # CIDSystemInfo dictionary - # A dictionary containing entries that define the character collection of the CIDFont. - newobj(); - out('<>'); - out('endobj'); - - # Font descriptor - # A font descriptor describing the CIDFont default metrics other than its glyph widths - newobj(); - out('<>'); - out('endobj'); - - # Embed CIDToGIDMap - # A specification of the mapping from CIDs to glyph indices - newobj(); - ctgfile = getfontpath(font['ctg']) - if (!ctgfile) - Error('Font file not found: ' + ctgfile); - end - size = File.size(ctgfile); - out('<>'); - open(ctgfile, "rb") do |f| - putstream(f.read()) - end - out('endobj'); - end - - # - # Converts UTF-8 strings to codepoints array.
    - # Invalid byte sequences will be replaced with 0xFFFD (replacement character)
    - # Based on: http://www.faqs.org/rfcs/rfc3629.html - #
    -	# 	  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.
    - # Based on: http://www.faqs.org/rfcs/rfc2781.html - #
    -	#   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.
    - # Supports: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, ins, del, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small - # @param string :html text to display - # @param boolean :ln if true add a new line after text (default = true) - # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. - # - def writeHTML(html, ln=true, fill=0, h=0) - - @lasth = h if h > 0 - if (@lasth == 0) - #set row height - @lasth = @font_size * @@k_cell_height_ratio; - end - - @href = nil - @style = ""; - @t_cells = [[]]; - @table_id = 0; - - # pre calculate - html.split(/(<[^>]+>)/).each do |element| - if "<" == element[0,1] - #Tag - if (element[1, 1] == '/') - closedHTMLTagCalc(element[2..-2].downcase); - else - #Extract attributes - # get tag name - tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} - tag = tag[0].to_s.downcase; - - # get attributes - attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) - attrs = {} - attr_array.each do |name, value| - attrs[name.downcase] = value; - end - openHTMLTagCalc(tag, attrs); - end - end - end - @table_id = 0; - - html.split(/(<[A-Za-z!?\/][^>]*?>)/).each do |element| - if "<" == element[0,1] - #Tag - if (element[1, 1] == '/') - closedHTMLTagHandler(element[2..-2].downcase); - else - #Extract attributes - # get tag name - tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} - tag = tag[0].to_s.downcase; - - # get attributes - attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) - attrs = {} - attr_array.each do |name, value| - attrs[name.downcase] = value; - end - openHTMLTagHandler(tag, attrs, fill); - end - - else - #Text - if (@tdbegin) - element.gsub!(/[\t\r\n\f]/, ""); - @tdtext << element.gsub(/ /, " "); - elsif (@href) - element.gsub!(/[\t\r\n\f]/, ""); - addHtmlLink(@href, element, fill); - elsif (@pre_state == true and element.length > 0) - Write(@lasth, unhtmlentities(element), '', fill); - elsif (element.strip.length > 0) - element.gsub!(/[\t\r\n\f]/, ""); - element.gsub!(/ /, " "); - Write(@lasth, unhtmlentities(element), '', fill); - end - end - end - - if (ln) - Ln(@lasth); - end - end - alias_method :write_html, :writeHTML - - # - # Prints a cell (rectangular area) with optional borders, background color and html text string. The upper-left corner of the cell corresponds to the current position. After the call, the current position moves to the right or to the next line.
    - # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. - # @param float :w Cell width. If 0, the cell extends up to the right margin. - # @param float :h Cell minimum height. The cell extends automatically if needed. - # @param float :x upper-left corner X coordinate - # @param float :y upper-left corner Y coordinate - # @param string :html html text to print. Default value: empty string. - # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    - # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line
    • 2: below
    -# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @see Cell() - # - def writeHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0) - - if (@lasth == 0) - #set row height - @lasth = @font_size * @@k_cell_height_ratio; - end - - if (x == 0) - x = GetX(); - end - if (y == 0) - y = GetY(); - end - - # get current page number - pagenum = @page; - - SetX(x); - SetY(y); - - if (w == 0) - w = @fw - x - @r_margin; - end - - b=0; - if (border) - if (border==1) - border='LTRB'; - b='LRT'; - b2='LR'; - elsif border.is_a?(String) - b2=''; - if (border.include?('L')) - b2<<'L'; - end - if (border.include?('R')) - b2<<'R'; - end - b=(border.include?('T')) ? b2 + 'T' : b2; - end - end - - # store original margin values - l_margin = @l_margin; - r_margin = @r_margin; - - # set new margin values - SetLeftMargin(x); - SetRightMargin(@fw - x - w); - - # calculate remaining vertical space on page - restspace = GetPageHeight() - GetY() - GetBreakMargin(); - - writeHTML(html, true, fill); # write html text - SetX(x) - - currentY = GetY(); - @auto_page_break = false; - # check if a new page has been created - if (@page > pagenum) - # design a cell around the text on first page - currentpage = @page; - @page = pagenum; - SetY(GetPageHeight() - restspace - GetBreakMargin()); - SetX(x) - Cell(w, restspace - 1, "", b, 0, 'L', 0); - b = b2; - @page += 1; - while @page < currentpage - SetY(@t_margin); # put cursor at the beginning of text - SetX(x) - Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0); - @page += 1; - end - if (border.is_a?(String) and border.include?('B')) - b<<'B'; - end - # design a cell around the text on last page - SetY(@t_margin); # put cursor at the beginning of text - SetX(x) - Cell(w, currentY - @t_margin, "", b, 0, 'L', 0); - else - SetY(y); # put cursor at the beginning of text - # design a cell around the text - SetX(x) - Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0); - end - @auto_page_break = true; - - # restore original margin values - SetLeftMargin(l_margin); - SetRightMargin(r_margin); - - @lasth = h - - # move cursor to specified position - if (ln == 0) - # go to the top-right of the cell - @x = x + w; - @y = y; - elsif (ln == 1) - # go to the beginning of the next line - @x = @l_margin; - @y = currentY; - elsif (ln == 2) - # go to the bottom-left of the cell (below) - @x = x; - @y = currentY; - end - end - alias_method :write_html_cell, :writeHTMLCell - - # - # Check html table tag position. - # - # @param array :table potision array - # @param int :current tr tag id number - # @param int :current td tag id number - # @access private - # @return int : next td_id position. - # value 0 mean that can use position. - # - def checkTableBlockingCellPosition(table, tr_id, td_id ) - 0.upto(tr_id) do |j| - 0.upto(@t_cells[table][j].size - 1) do |i| - if @t_cells[table][j][i]['i0'] <= td_id and td_id <= @t_cells[table][j][i]['i1'] - if @t_cells[table][j][i]['j0'] <= tr_id and tr_id <= @t_cells[table][j][i]['j1'] - return @t_cells[table][j][i]['i1'] - td_id + 1; - end - end - end - end - return 0; - end - - # - # Calculate opening tags. - # - # html table cell array : @t_cells - # - # i0: table cell start position - # i1: table cell end position - # j0: table row start position - # j1: table row end position - # - # +------+ - # |i0,j0 | - # | i1,j1| - # +------+ - # - # example html: - # - # - # - # - # - #
    - # - # i: 0 1 2 - # j+----+----+----+ - # :|0,0 |1,0 |2,0 | - # 0| 0,0| 1,0| 2,0| - # +----+----+----+ - # |0,1 |2,1 | - # 1| 1,1| 2,1| - # +----+----+----+ - # |0,2 |1,2 |2,2 | - # 2| | 1,2| 2,2| - # + +----+----+ - # | |1,3 |2,3 | - # 3| 0,3| 1,3| 2,3| - # +----+----+----+ - # - # html table cell array : - # [[[i0=>0,j0=>0,i1=>0,j1=>0],[i0=>1,j0=>0,i1=>1,j1=>0],[i0=>2,j0=>0,i1=>2,j1=>0]], - # [[i0=>0,j0=>1,i1=>1,j1=>1],[i0=>2,j0=>1,i1=>2,j1=>1]], - # [[i0=>0,j0=>2,i1=>0,j1=>3],[i0=>1,j0=>2,i1=>1,j1=>2],[i0=>2,j0=>2,i1=>2,j1=>2]] - # [[i0=>1,j0=>3,i1=>1,j1=>3],[i0=>2,j0=>3,i1=>2,j1=>3]]] - # - # @param string :tag tag name (in upcase) - # @param string :attr tag attribute (in upcase) - # @access private - # - def openHTMLTagCalc(tag, attrs) - #Opening tag - case (tag) - when 'table' - @max_table_columns[@table_id] = 0; - @t_columns = 0; - @tr_id = -1; - when 'tr' - if @max_table_columns[@table_id] < @t_columns - @max_table_columns[@table_id] = @t_columns; - end - @t_columns = 0; - @tr_id += 1; - @td_id = -1; - @t_cells[@table_id].push [] - when 'td', 'th' - @td_id += 1; - if attrs['colspan'].nil? or attrs['colspan'] == '' - colspan = 1; - else - colspan = attrs['colspan'].to_i; - end - if attrs['rowspan'].nil? or attrs['rowspan'] == '' - rowspan = 1; - else - rowspan = attrs['rowspan'].to_i; - end - - i = 0; - while true - next_i_distance = checkTableBlockingCellPosition(@table_id, @tr_id, @td_id + i); - if next_i_distance == 0 - @t_cells[@table_id][@tr_id].push "i0"=>@td_id + i, "j0"=>@tr_id, "i1"=>(@td_id + i + colspan - 1), "j1"=>@tr_id + rowspan - 1 - break; - end - i += next_i_distance; - end - - @t_columns += colspan; - end - end - - # - # Calculate closing tags. - # @param string :tag tag name (in upcase) - # @access private - # - def closedHTMLTagCalc(tag) - #Closing tag - case (tag) - when 'table' - if @max_table_columns[@table_id] < @t_columns - @max_table_columns[@table_id] = @t_columns; - end - @table_id += 1; - @t_cells.push [] - end - end - - # - # Convert to accessible file path - # @param string :attrname image file name - # - def getImageFilename( attrname ) - nil - end - - # - # Process opening tags. - # @param string :tag tag name (in upcase) - # @param string :attr tag attribute (in upcase) - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @access private - # - def openHTMLTagHandler(tag, attrs, fill=0) - #Opening tag - case (tag) - when 'pre' - @pre_state = true; - @l_margin += 5; - @r_margin += 5; - @x += 5; - - when 'table' - Ln(); - if @default_table_columns < @max_table_columns[@table_id] - @table_columns = @max_table_columns[@table_id]; - else - @table_columns = @default_table_columns; - end - @l_margin += 5; - @r_margin += 5; - @x += 5; - - if attrs['border'].nil? or attrs['border'] == '' - @tableborder = 0; - else - @tableborder = attrs['border']; - end - @tr_id = -1; - @max_td_page[0] = @page; - @max_td_y[0] = @y; - - when 'tr', 'td', 'th' - if tag == 'th' - SetStyle('b', true); - @tdalign = "C"; - end - if ((!attrs['width'].nil?) and (attrs['width'] != '')) - @tdwidth = (attrs['width'].to_i/4); - else - @tdwidth = ((@w - @l_margin - @r_margin) / @table_columns); - end - - if tag == 'tr' - @tr_id += 1; - @td_id = -1; - else - @td_id += 1; - @x = @l_margin + @tdwidth * @t_cells[@table_id][@tr_id][@td_id]['i0']; - end - - if attrs['colspan'].nil? or attrs['border'] == '' - @colspan = 1; - else - @colspan = attrs['colspan'].to_i; - end - @tdwidth *= @colspan; - if ((!attrs['height'].nil?) and (attrs['height'] != '')) - @tdheight=(attrs['height'].to_i / @k); - else - @tdheight = @lasth; - end - if ((!attrs['align'].nil?) and (attrs['align'] != '')) - case (attrs['align']) - when 'center' - @tdalign = "C"; - when 'right' - @tdalign = "R"; - when 'left' - @tdalign = "L"; - end - end - if ((!attrs['bgcolor'].nil?) and (attrs['bgcolor'] != '')) - coul = convertColorHexToDec(attrs['bgcolor']); - SetFillColor(coul['R'], coul['G'], coul['B']); - @tdfill=1; - end - @tdbegin=true; - - when 'hr' - margin = 1; - if ((!attrs['width'].nil?) and (attrs['width'] != '')) - hrWidth = attrs['width']; - else - hrWidth = @w - @l_margin - @r_margin - margin; - end - SetLineWidth(0.2); - Line(@x + margin, @y, @x + hrWidth, @y); - Ln(); - - when 'strong' - SetStyle('b', true); - - when 'em' - SetStyle('i', true); - - when 'ins' - SetStyle('u', true); - - when 'del' - SetStyle('d', true); - - when 'b', 'i', 'u' - SetStyle(tag, true); - - when 'a' - @href = attrs['href']; - - when 'img' - if (!attrs['src'].nil?) - # Don't generates image inside table tag - if (@tdbegin) - @tdtext << attrs['src']; - return - end - # Only generates image include a pdf if RMagick is avalaible - unless Object.const_defined?(:Magick) - Write(@lasth, attrs['src'], '', fill); - return - end - file = getImageFilename(attrs['src']) - if (file.nil?) - Write(@lasth, attrs['src'], '', fill); - return - end - - if (attrs['width'].nil?) - attrs['width'] = 0; - end - if (attrs['height'].nil?) - attrs['height'] = 0; - end - - begin - Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height'])); - #SetX(@img_rb_x); - SetY(@img_rb_y); - rescue => err - logger.error "pdf: Image: error: #{err.message}" - Write(@lasth, attrs['src'], '', fill); - end - end - - when 'ul', 'ol' - if @li_count == 0 - Ln() if @prevquote_count == @quote_count; # insert Ln for keeping quote lines - @prevquote_count = @quote_count; - end - if @li_state == true - Ln(); - @li_state = false; - end - if tag == 'ul' - @list_ordered[@li_count] = false; - else - @list_ordered[@li_count] = true; - end - @list_count[@li_count] = 0; - @li_count += 1 - - when 'li' - Ln() if @li_state == true - if (@list_ordered[@li_count - 1]) - @list_count[@li_count - 1] += 1; - @li_spacer = " " * @li_count + (@list_count[@li_count - 1]).to_s + ". "; - else - #unordered list simbol - @li_spacer = " " * @li_count + "- "; - end - Write(@lasth, @spacer + @li_spacer, '', fill); - @li_state = true; - - when 'blockquote' - if (@quote_count == 0) - SetStyle('i', true); - @l_margin += 5; - else - @l_margin += 5 / 2; - end - @x = @l_margin; - @quote_top[@quote_count] = @y; - @quote_page[@quote_count] = @page; - @quote_count += 1 - when 'br' - if @tdbegin - @tdtext << "\n" - return - end - Ln(); - - if (@li_spacer.length > 0) - @x += GetStringWidth(@li_spacer); - end - - when 'p' - Ln(); - 0.upto(@quote_count - 1) do |i| - if @quote_page[i] == @page; - if @quote_top[i] == @y - @lasth; # fix start line - @quote_top[i] = @y; - end - else - if @quote_page[i] == @page - 1; - @quote_page[i] = @page; # fix start line - @quote_top[i] = @t_margin; - end - end - end - - when 'sup' - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt * @@k_small_ratio); - SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'sub' - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt * @@k_small_ratio); - SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'small' - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt * @@k_small_ratio); - SetXY(GetX(), GetY() + ((currentfont_size - @font_size)/3)); - - when 'font' - if (!attrs['color'].nil? and attrs['color']!='') - coul = convertColorHexToDec(attrs['color']); - SetTextColor(coul['R'], coul['G'], coul['B']); - @issetcolor=true; - end - if (!attrs['face'].nil? and @fontlist.include?(attrs['face'].downcase)) - SetFont(attrs['face'].downcase); - @issetfont=true; - end - if (!attrs['size'].nil?) - headsize = attrs['size'].to_i; - else - headsize = 0; - end - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt + headsize); - @lasth = @font_size * @@k_cell_height_ratio; - - when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' - Ln(); - headsize = (4 - tag[1,1].to_f) * 2 - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt + headsize); - SetStyle('b', true); - @lasth = @font_size * @@k_cell_height_ratio; - - end - end - - # - # Process closing tags. - # @param string :tag tag name (in upcase) - # @access private - # - def closedHTMLTagHandler(tag) - #Closing tag - case (tag) - when 'pre' - @pre_state = false; - @l_margin -= 5; - @r_margin -= 5; - @x = @l_margin; - Ln(); - - when 'td','th' - base_page = @page; - base_x = @x; - base_y = @y; - - MultiCell(@tdwidth, @tdheight, unhtmlentities(@tdtext.strip), @tableborder, @tdalign, @tdfill, 1); - tr_end = @t_cells[@table_id][@tr_id][@td_id]['j1'] + 1; - if @max_td_page[tr_end].nil? or (@max_td_page[tr_end] < @page) - @max_td_page[tr_end] = @page - @max_td_y[tr_end] = @y - elsif (@max_td_page[tr_end] == @page) - @max_td_y[tr_end] = @y if @max_td_y[tr_end].nil? or (@max_td_y[tr_end] < @y) - end - - @page = base_page; - @x = base_x + @tdwidth; - @y = base_y; - @tdtext = ''; - @tdbegin = false; - @tdwidth = 0; - @tdheight = 0; - @tdalign = "L"; - SetStyle('b', false); - @tdfill = 0; - SetFillColor(@prevfill_color[0], @prevfill_color[1], @prevfill_color[2]); - - when 'tr' - @y = @max_td_y[@tr_id + 1]; - @x = @l_margin; - @page = @max_td_page[@tr_id + 1]; - - when 'table' - # Write Table Line - width = (@w - @l_margin - @r_margin) / @table_columns; - 0.upto(@t_cells[@table_id].size - 1) do |j| - 0.upto(@t_cells[@table_id][j].size - 1) do |i| - @page = @max_td_page[j] - i0=@t_cells[@table_id][j][i]['i0']; - j0=@t_cells[@table_id][j][i]['j0']; - i1=@t_cells[@table_id][j][i]['i1']; - j1=@t_cells[@table_id][j][i]['j1']; - - Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j0]) # top - if ( @page == @max_td_page[j1 + 1]) - Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @max_td_y[j1+1]) # left - Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j1+1]) # right - else - Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @page_break_trigger) # left - Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @page_break_trigger) # right - @page += 1; - while @page < @max_td_page[j1 + 1] - Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @page_break_trigger) # left - Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @page_break_trigger) # right - @page += 1; - end - Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @max_td_y[j1+1]) # left - Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @max_td_y[j1+1]) # right - end - Line(@l_margin + width * i0, @max_td_y[j1+1], @l_margin + width * (i1+1), @max_td_y[j1+1]) # bottom - end - end - - @l_margin -= 5; - @r_margin -= 5; - @tableborder=0; - @table_id += 1; - - when 'strong' - SetStyle('b', false); - - when 'em' - SetStyle('i', false); - - when 'ins' - SetStyle('u', false); - - when 'del' - SetStyle('d', false); - - when 'b', 'i', 'u' - SetStyle(tag, false); - - when 'a' - @href = nil; - - when 'p' - Ln(); - - when 'sup' - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'sub' - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'small' - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetXY(GetX(), GetY() - ((@font_size - currentfont_size)/3)); - - when 'font' - if (@issetcolor == true) - SetTextColor(@prevtext_color[0], @prevtext_color[1], @prevtext_color[2]); - end - if (@issetfont) - @font_family = @prevfont_family; - @font_style = @prevfont_style; - SetFont(@font_family); - @issetfont = false; - end - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - #@text_color = @prevtext_color; - @lasth = @font_size * @@k_cell_height_ratio; - - when 'blockquote' - @quote_count -= 1 - if (@quote_page[@quote_count] == @page) - Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @y) # quoto line - else - cur_page = @page; - cur_y = @y; - @page = @quote_page[@quote_count]; - if (@quote_top[@quote_count] < @page_break_trigger) - Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @page_break_trigger) # quoto line - end - @page += 1; - while @page < cur_page - Line(@l_margin - 1, @t_margin, @l_margin - 1, @page_break_trigger) # quoto line - @page += 1; - end - @y = cur_y; - Line(@l_margin - 1, @t_margin, @l_margin - 1, @y) # quoto line - end - if (@quote_count <= 0) - SetStyle('i', false); - @l_margin -= 5; - else - @l_margin -= 5 / 2; - end - @x = @l_margin; - Ln() if @quote_count == 0 - - when 'ul', 'ol' - @li_count -= 1 - if @li_state == true - Ln(); - @li_state = false; - end - - when 'li' - @li_spacer = ""; - if @li_state == true - Ln(); - @li_state = false; - end - - when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetStyle('b', false); - Ln(); - @lasth = @font_size * @@k_cell_height_ratio; - - if tag == 'h1' or tag == 'h2' or tag == 'h3' or tag == 'h4' - margin = 1; - hrWidth = @w - @l_margin - @r_margin - margin; - if tag == 'h1' or tag == 'h2' - SetLineWidth(0.2); - else - SetLineWidth(0.1); - end - Line(@x + margin, @y, @x + hrWidth, @y); - end - end - end - - # - # Sets font style. - # @param string :tag tag name (in lowercase) - # @param boolean :enable - # @access private - # - def SetStyle(tag, enable) - #Modify style and select corresponding font - ['b', 'i', 'u', 'd'].each do |s| - if tag.downcase == s - if enable - @style << s if ! @style.include?(s) - else - @style = @style.gsub(s,'') - end - end - end - SetFont('', @style); - end - - # - # Output anchor link. - # @param string :url link URL - # @param string :name link name - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @access public - # - def addHtmlLink(url, name, fill=0) - #Put a hyperlink - SetTextColor(0, 0, 255); - SetStyle('u', true); - Write(@lasth, name, url, fill); - SetStyle('u', false); - SetTextColor(0); - end - - # - # Returns an associative array (keys: R,G,B) from - # a hex html code (e.g. #3FE5AA). - # @param string :color hexadecimal html color [#rrggbb] - # @return array - # @access private - # - def convertColorHexToDec(color = "#000000") - tbl_color = {} - tbl_color['R'] = color[1,2].hex.to_i; - tbl_color['G'] = color[3,2].hex.to_i; - tbl_color['B'] = color[5,2].hex.to_i; - return tbl_color; - end - - # - # Converts pixels to millimeters in 72 dpi. - # @param int :px pixels - # @return float millimeters - # @access private - # - def pixelsToMillimeters(px) - return px.to_f * 25.4 / 72; - end - - # - # Reverse function for htmlentities. - # Convert entities in UTF-8. - # - # @param :text_to_convert Text to convert. - # @return string converted - # - def unhtmlentities(string) - CGI.unescapeHTML(string) - end - -end # END OF CLASS - -#TODO 2007-05-25 (EJM) Level=0 - -#Handle special IE contype request -# if (!_SERVER['HTTP_USER_AGENT'].nil? and (_SERVER['HTTP_USER_AGENT']=='contype')) -# header('Content-Type: application/pdf'); -# exit; -# } diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09608744e321935ef94b4022df67d37162d9f786.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09608744e321935ef94b4022df67d37162d9f786.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,757 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 MailerTest < ActiveSupport::TestCase + include Redmine::I18n + include ActionDispatch::Assertions::SelectorAssertions + fixtures :projects, :enabled_modules, :issues, :users, :members, + :member_roles, :roles, :documents, :attachments, :news, + :tokens, :journals, :journal_details, :changesets, + :trackers, :projects_trackers, + :issue_statuses, :enumerations, :messages, :boards, :repositories, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, + :versions, + :comments + + def setup + ActionMailer::Base.deliveries.clear + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'http' + Setting.plain_text_mail = '0' + end + + def test_generated_links_in_emails + Setting.default_language = 'en' + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'https' + + journal = Journal.find(3) + assert Mailer.deliver_issue_edit(journal) + + mail = last_email + assert_not_nil mail + + assert_select_email do + # link to the main ticket + assert_select 'a[href=?]', + 'https://mydomain.foo/issues/2#change-3', + :text => 'Feature request #2: Add ingredients categories' + # link to a referenced ticket + assert_select 'a[href=?][title=?]', + 'https://mydomain.foo/issues/1', + 'Can't print recipes (New)', + :text => '#1' + # link to a changeset + assert_select 'a[href=?][title=?]', + 'https://mydomain.foo/projects/ecookbook/repository/revisions/2', + 'This commit fixes #1, #2 and references #1 & #3', + :text => 'r2' + # link to a description diff + assert_select 'a[href=?][title=?]', + 'https://mydomain.foo/journals/diff/3?detail_id=4', + 'View differences', + :text => 'diff' + # link to an attachment + assert_select 'a[href=?]', + 'https://mydomain.foo/attachments/download/4/source.rb', + :text => 'source.rb' + end + end + + def test_generated_links_with_prefix + Setting.default_language = 'en' + relative_url_root = Redmine::Utils.relative_url_root + Setting.host_name = 'mydomain.foo/rdm' + Setting.protocol = 'http' + + journal = Journal.find(3) + assert Mailer.deliver_issue_edit(journal) + + mail = last_email + assert_not_nil mail + + assert_select_email do + # link to the main ticket + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/issues/2#change-3', + :text => 'Feature request #2: Add ingredients categories' + # link to a referenced ticket + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/issues/1', + 'Can't print recipes (New)', + :text => '#1' + # link to a changeset + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2', + 'This commit fixes #1, #2 and references #1 & #3', + :text => 'r2' + # link to a description diff + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4', + 'View differences', + :text => 'diff' + # link to an attachment + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/attachments/download/4/source.rb', + :text => 'source.rb' + end + end + + def test_issue_edit_should_generate_url_with_hostname_for_relations + journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now) + journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2) + Mailer.deliver_issue_edit(journal) + assert_not_nil last_email + assert_select_email do + assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2' + end + end + + def test_generated_links_with_prefix_and_no_relative_url_root + Setting.default_language = 'en' + relative_url_root = Redmine::Utils.relative_url_root + Setting.host_name = 'mydomain.foo/rdm' + Setting.protocol = 'http' + Redmine::Utils.relative_url_root = nil + + journal = Journal.find(3) + assert Mailer.deliver_issue_edit(journal) + + mail = last_email + assert_not_nil mail + + assert_select_email do + # link to the main ticket + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/issues/2#change-3', + :text => 'Feature request #2: Add ingredients categories' + # link to a referenced ticket + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/issues/1', + 'Can't print recipes (New)', + :text => '#1' + # link to a changeset + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2', + 'This commit fixes #1, #2 and references #1 & #3', + :text => 'r2' + # link to a description diff + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4', + 'View differences', + :text => 'diff' + # link to an attachment + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/attachments/download/4/source.rb', + :text => 'source.rb' + end + ensure + # restore it + Redmine::Utils.relative_url_root = relative_url_root + end + + def test_email_headers + issue = Issue.find(1) + Mailer.deliver_issue_add(issue) + mail = last_email + assert_not_nil mail + assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s + assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s + assert_equal '', mail.header['List-Id'].to_s + end + + def test_email_headers_should_include_sender + issue = Issue.find(1) + Mailer.deliver_issue_add(issue) + mail = last_email + assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s + end + + def test_plain_text_mail + Setting.plain_text_mail = 1 + journal = Journal.find(2) + Mailer.deliver_issue_edit(journal) + mail = last_email + assert_equal "text/plain; charset=UTF-8", mail.content_type + assert_equal 0, mail.parts.size + assert !mail.encoded.include?('href') + end + + def test_html_mail + Setting.plain_text_mail = 0 + journal = Journal.find(2) + Mailer.deliver_issue_edit(journal) + mail = last_email + assert_equal 2, mail.parts.size + assert mail.encoded.include?('href') + end + + def test_from_header + with_settings :mail_from => 'redmine@example.net' do + Mailer.test_email(User.find(1)).deliver + end + mail = last_email + assert_equal 'redmine@example.net', mail.from_addrs.first + end + + def test_from_header_with_phrase + with_settings :mail_from => 'Redmine app ' do + Mailer.test_email(User.find(1)).deliver + end + mail = last_email + assert_equal 'redmine@example.net', mail.from_addrs.first + assert_equal 'Redmine app ', mail.header['From'].to_s + end + + def test_should_not_send_email_without_recipient + news = News.first + user = news.author + # Remove members except news author + news.project.memberships.each {|m| m.destroy unless m.user == user} + + user.pref.no_self_notified = false + user.pref.save + User.current = user + Mailer.news_added(news.reload).deliver + assert_equal 1, last_email.bcc.size + + # nobody to notify + user.pref.no_self_notified = true + user.pref.save + User.current = user + ActionMailer::Base.deliveries.clear + Mailer.news_added(news.reload).deliver + assert ActionMailer::Base.deliveries.empty? + end + + def test_issue_add_message_id + issue = Issue.find(2) + Mailer.deliver_issue_add(issue) + mail = last_email + assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.issue-2.20060719190421@example.net", mail.references + end + + def test_issue_edit_message_id + journal = Journal.find(3) + journal.issue = Issue.find(2) + + Mailer.deliver_issue_edit(journal) + mail = last_email + assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.issue-2.20060719190421@example.net", mail.references + assert_select_email do + # link to the update + assert_select "a[href=?]", + "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}" + end + end + + def test_message_posted_message_id + message = Message.find(1) + Mailer.message_posted(message).deliver + mail = last_email + assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.message-1.20070512151532@example.net", mail.references + assert_select_email do + # link to the message + assert_select "a[href=?]", + "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}", + :text => message.subject + end + end + + def test_reply_posted_message_id + message = Message.find(3) + Mailer.message_posted(message).deliver + mail = last_email + assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.message-1.20070512151532@example.net", mail.references + assert_select_email do + # link to the reply + assert_select "a[href=?]", + "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}", + :text => message.subject + end + end + + test "#issue_add should notify project members" do + issue = Issue.find(1) + assert Mailer.deliver_issue_add(issue) + assert last_email.bcc.include?('dlopper@somenet.foo') + end + + test "#issue_add should not notify project members that are not allow to view the issue" do + issue = Issue.find(1) + Role.find(2).remove_permission!(:view_issues) + assert Mailer.deliver_issue_add(issue) + assert !last_email.bcc.include?('dlopper@somenet.foo') + end + + test "#issue_add should notify issue watchers" do + issue = Issue.find(1) + user = User.find(9) + # minimal email notification options + user.pref.no_self_notified = '1' + user.pref.save + user.mail_notification = false + user.save + + Watcher.create!(:watchable => issue, :user => user) + assert Mailer.deliver_issue_add(issue) + assert last_email.bcc.include?(user.mail) + end + + test "#issue_add should not notify watchers not allowed to view the issue" do + issue = Issue.find(1) + user = User.find(9) + Watcher.create!(:watchable => issue, :user => user) + Role.non_member.remove_permission!(:view_issues) + assert Mailer.deliver_issue_add(issue) + assert !last_email.bcc.include?(user.mail) + end + + def test_issue_add_should_include_enabled_fields + Setting.default_language = 'en' + issue = Issue.find(2) + assert Mailer.deliver_issue_add(issue) + assert_mail_body_match '* Target version: 1.0', last_email + assert_select_email do + assert_select 'li', :text => 'Target version: 1.0' + end + end + + def test_issue_add_should_not_include_disabled_fields + Setting.default_language = 'en' + issue = Issue.find(2) + tracker = issue.tracker + tracker.core_fields -= ['fixed_version_id'] + tracker.save! + assert Mailer.deliver_issue_add(issue) + assert_mail_body_no_match 'Target version', last_email + assert_select_email do + assert_select 'li', :text => /Target version/, :count => 0 + end + end + + # test mailer methods for each language + def test_issue_add + issue = Issue.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.deliver_issue_add(issue) + end + end + + def test_issue_edit + journal = Journal.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.deliver_issue_edit(journal) + end + end + + def test_issue_edit_should_send_private_notes_to_users_with_permission_only + journal = Journal.find(1) + journal.private_notes = true + journal.save! + + Role.find(2).add_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort + + Role.find(2).remove_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort + end + + def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only + Issue.find(1).set_watcher(User.find_by_login('someone')) + journal = Journal.find(1) + journal.private_notes = true + journal.save! + + Role.non_member.add_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort + + Role.non_member.remove_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort + end + + def test_issue_edit_should_mark_private_notes + journal = Journal.find(2) + journal.private_notes = true + journal.save! + + with_settings :default_language => 'en' do + Mailer.deliver_issue_edit(journal) + end + assert_mail_body_match '(Private notes)', last_email + end + + def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue + issue = Issue.generate! + private_issue = Issue.generate!(:is_private => true) + IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates') + issue.reload + assert_equal 1, issue.journals.size + journal = issue.journals.first + ActionMailer::Base.deliveries.clear + + Mailer.deliver_issue_edit(journal) + last_email.bcc.each do |email| + user = User.find_by_mail(email) + assert private_issue.visible?(user), "Issue was not visible to #{user}" + end + end + + def test_document_added + document = Document.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.document_added(document).deliver + end + end + + def test_attachments_added + attachements = [ Attachment.find_by_container_type('Document') ] + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.attachments_added(attachements).deliver + end + end + + def test_version_file_added + attachements = [ Attachment.find_by_container_type('Version') ] + assert Mailer.attachments_added(attachements).deliver + assert_not_nil last_email.bcc + assert last_email.bcc.any? + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" + end + end + + def test_project_file_added + attachements = [ Attachment.find_by_container_type('Project') ] + assert Mailer.attachments_added(attachements).deliver + assert_not_nil last_email.bcc + assert last_email.bcc.any? + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" + end + end + + def test_news_added + news = News.first + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.news_added(news).deliver + end + end + + def test_news_comment_added + comment = Comment.find(2) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.news_comment_added(comment).deliver + end + end + + def test_message_posted + message = Message.first + recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author} + recipients = recipients.compact.uniq + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.message_posted(message).deliver + end + end + + def test_wiki_content_added + content = WikiContent.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert_difference 'ActionMailer::Base.deliveries.size' do + assert Mailer.wiki_content_added(content).deliver + assert_select_email do + assert_select 'a[href=?]', + 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation', + :text => 'CookBook documentation' + end + end + end + end + + def test_wiki_content_updated + content = WikiContent.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert_difference 'ActionMailer::Base.deliveries.size' do + assert Mailer.wiki_content_updated(content).deliver + assert_select_email do + assert_select 'a[href=?]', + 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation', + :text => 'CookBook documentation' + end + end + end + end + + def test_account_information + user = User.find(2) + valid_languages.each do |lang| + user.update_attribute :language, lang.to_s + user.reload + assert Mailer.account_information(user, 'pAsswORd').deliver + end + end + + def test_lost_password + token = Token.find(2) + valid_languages.each do |lang| + token.user.update_attribute :language, lang.to_s + token.reload + assert Mailer.lost_password(token).deliver + end + end + + def test_register + token = Token.find(1) + Setting.host_name = 'redmine.foo' + Setting.protocol = 'https' + + valid_languages.each do |lang| + token.user.update_attribute :language, lang.to_s + token.reload + ActionMailer::Base.deliveries.clear + assert Mailer.register(token).deliver + mail = last_email + assert_select_email do + assert_select "a[href=?]", + "https://redmine.foo/account/activate?token=#{token.value}", + :text => "https://redmine.foo/account/activate?token=#{token.value}" + end + end + end + + def test_test + user = User.find(1) + valid_languages.each do |lang| + user.update_attribute :language, lang.to_s + assert Mailer.test_email(user).deliver + end + end + + def test_reminders + Mailer.reminders(:days => 42) + assert_equal 1, ActionMailer::Base.deliveries.size + mail = last_email + assert mail.bcc.include?('dlopper@somenet.foo') + assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail + assert_equal '1 issue(s) due in the next 42 days', mail.subject + end + + def test_reminders_should_not_include_closed_issues + with_settings :default_language => 'en' do + Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5, + :subject => 'Closed issue', :assigned_to_id => 3, + :due_date => 5.days.from_now, + :author_id => 2) + ActionMailer::Base.deliveries.clear + + Mailer.reminders(:days => 42) + assert_equal 1, ActionMailer::Base.deliveries.size + mail = last_email + assert mail.bcc.include?('dlopper@somenet.foo') + assert_mail_body_no_match 'Closed issue', mail + end + end + + def test_reminders_for_users + Mailer.reminders(:days => 42, :users => ['5']) + assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper + Mailer.reminders(:days => 42, :users => ['3']) + assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper + mail = last_email + assert mail.bcc.include?('dlopper@somenet.foo') + assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail + end + + def test_reminder_should_include_issues_assigned_to_groups + with_settings :default_language => 'en' do + group = Group.generate! + group.users << User.find(2) + group.users << User.find(3) + + Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, + :subject => 'Assigned to group', :assigned_to => group, + :due_date => 5.days.from_now, + :author_id => 2) + ActionMailer::Base.deliveries.clear + + Mailer.reminders(:days => 7) + assert_equal 2, ActionMailer::Base.deliveries.size + assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort + ActionMailer::Base.deliveries.each do |mail| + assert_mail_body_match 'Assigned to group', mail + end + end + end + + def test_mailer_should_not_change_locale + Setting.default_language = 'en' + # Set current language to italian + set_language_if_valid 'it' + # Send an email to a french user + user = User.find(1) + user.language = 'fr' + Mailer.account_activated(user).deliver + mail = last_email + assert_mail_body_match 'Votre compte', mail + + assert_equal :it, current_language + end + + def test_with_deliveries_off + Mailer.with_deliveries false do + Mailer.test_email(User.find(1)).deliver + end + assert ActionMailer::Base.deliveries.empty? + # should restore perform_deliveries + assert ActionMailer::Base.perform_deliveries + end + + def test_layout_should_include_the_emails_header + with_settings :emails_header => "*Header content*" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".header" do + assert_select "strong", :text => "Header content" + end + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_include "*Header content*", mail.body.decoded + end + end + end + + def test_layout_should_not_include_empty_emails_header + with_settings :emails_header => "", :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".header", false + end + end + end + + def test_layout_should_include_the_emails_footer + with_settings :emails_footer => "*Footer content*" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".footer" do + assert_select "strong", :text => "Footer content" + end + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_include "\n-- \n", mail.body.decoded + assert_include "*Footer content*", mail.body.decoded + end + end + end + + def test_layout_should_not_include_empty_emails_footer + with_settings :emails_footer => "" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".footer", false + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_not_include "\n-- \n", mail.body.decoded + end + end + end + + def test_should_escape_html_templates_only + Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a ') + mail = last_email + assert_equal 2, mail.parts.size + assert_include '', text_part.body.encoded + assert_include '<tag>', html_part.body.encoded + end + + def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true + mail = Mailer.test_email(User.find(1)) + mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) + + ActionMailer::Base.raise_delivery_errors = true + assert_raise Exception, "delivery error" do + mail.deliver + end + ensure + ActionMailer::Base.raise_delivery_errors = false + end + + def test_should_log_delivery_errors_when_raise_delivery_errors_is_false + mail = Mailer.test_email(User.find(1)) + mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) + + Rails.logger.expects(:error).with("Email delivery error: delivery error") + ActionMailer::Base.raise_delivery_errors = false + assert_nothing_raised do + mail.deliver + end + end + + def test_mail_should_return_a_mail_message + assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1)) + end + + private + + def last_email + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + mail + end + + def text_part + last_email.parts.detect {|part| part.content_type.include?('text/plain')} + end + + def html_part + last_email.parts.detect {|part| part.content_type.include?('text/html')} + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/0981b558fa75377b5bf7b445e64b1ae66cad712f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/0981b558fa75377b5bf7b445e64b1ae66cad712f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,31 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::NumericFieldFormatTest < ActionView::TestCase + include ApplicationHelper + + def test_integer_field_with_url_pattern_should_format_as_link + field = IssueCustomField.new(:field_format => 'int', :url_pattern => 'http://foo/%value%') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "3") + + assert_equal 3, field.format.formatted_custom_value(self, custom_value, false) + assert_equal '3', field.format.formatted_custom_value(self, custom_value, true) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/099fe610435cd5107ad5a4a6139377c6eab6f41e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/099fe610435cd5107ad5a4a6139377c6eab6f41e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,46 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 WikiFormatting + module Textile + module Helper + def wikitoolbar_for(field_id) + heads_for_wiki_formatter + # Is there a simple way to link to a public resource? + url = "#{Redmine::Utils.relative_url_root}/help/#{current_language.to_s.downcase}/wiki_syntax.html" + javascript_tag("var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript url}'); wikiToolbar.draw();") + end + + def initial_page_content(page) + "h1. #{@page.pretty_title}" + end + + def heads_for_wiki_formatter + unless @heads_for_wiki_formatter_included + content_for :header_tags do + javascript_include_tag('jstoolbar/jstoolbar-textile.min') + + javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") + + stylesheet_link_tag('jstoolbar') + end + @heads_for_wiki_formatter_included = true + end + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09c2ce49203d7971d2ffb1539d86601fbcb17a3b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09c2ce49203d7971d2ffb1539d86601fbcb17a3b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,93 @@ +
    + <%= link_to(l(:label_version_new), new_project_version_path(@project), + :class => 'icon icon-add') if User.current.allowed_to?(:manage_versions, @project) %> +
    + +

    <%=l(:label_roadmap)%>

    + +<% if @versions.empty? %> +

    <%= l(:label_no_data) %>

    +<% else %> +
    + <% @versions.each do |version| %> +

    <%= link_to_version version, :name => version_anchor(version) %>

    + <%= render :partial => 'versions/overview', :locals => {:version => version} %> + <%= render(:partial => "wiki/content", + :locals => {:content => version.wiki_page.content}) if version.wiki_page %> + <% if (issues = @issues_by_version[version]) && issues.size > 0 %> + <%= form_tag({}) do -%> + + + <% issues.each do |issue| -%> + + + + + <% end -%> + + <% end %> + <% end %> + <%= call_hook :view_projects_roadmap_version_bottom, :version => version %> + <% end %> +
    +<% end %> + +<% content_for :sidebar do %> +<%= form_tag({}, :method => :get) do %> +

    <%= l(:label_roadmap) %>

    +
      +<% @trackers.each do |tracker| %> +
    • + +
    • +<% end %> +
    +

    +
      +
    • + +
    • + <% if @project.descendants.active.any? %> +
    • + <%= hidden_field_tag 'with_subprojects', 0 %> + +
    • + <% end %> +
    +

    <%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>

    +<% end %> + +

    <%= l(:label_version_plural) %>

    +
      +<% @versions.each do |version| %> +
    • + <%= link_to(format_version_name(version), "##{version_anchor(version)}") %> +
    • +<% end %> +
    +<% if @completed_versions.present? %> +

    + <%= 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 d98d22a98252 -r fb9a13467253 .svn/pristine/09/09c5ab3ad78ad355d7e262001a52b532d5297617.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09c5ab3ad78ad355d7e262001a52b532d5297617.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,759 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::Helpers::GanttHelperTest < ActionView::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :journals, :journal_details, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions, + :groups_users + + include ProjectsHelper + include IssuesHelper + include ERB::Util + include Rails.application.routes.url_helpers + + def setup + setup_with_controller + User.current = User.find(1) + end + + def today + @today ||= Date.today + end + private :today + + # Creates a Gantt chart for a 4 week span + def create_gantt(project=Project.generate!, options={}) + @project = project + @gantt = Redmine::Helpers::Gantt.new(options) + @gantt.project = @project + @gantt.query = IssueQuery.create!(:project => @project, :name => 'Gantt') + @gantt.view = self + @gantt.instance_variable_set('@date_from', options[:date_from] || (today - 14)) + @gantt.instance_variable_set('@date_to', options[:date_to] || (today + 14)) + end + private :create_gantt + + test "#number_of_rows with one project should return the number of rows just for that project" do + p1, p2 = Project.generate!, Project.generate! + i1, i2 = Issue.generate!(:project => p1), Issue.generate!(:project => p2) + create_gantt(p1) + assert_equal 2, @gantt.number_of_rows + end + + test "#number_of_rows with no project should return the total number of rows for all the projects, resursively" do + p1, p2 = Project.generate!, Project.generate! + create_gantt(nil) + # fix the return value of #number_of_rows_on_project() to an arbitrary value + # so that we really only test #number_of_rows + @gantt.stubs(:number_of_rows_on_project).returns(7) + # also fix #projects because we want to test #number_of_rows in isolation + @gantt.stubs(:projects).returns(Project.all) + # actual test + assert_equal Project.count*7, @gantt.number_of_rows + end + + test "#number_of_rows should not exceed max_rows option" do + p = Project.generate! + 5.times do + Issue.generate!(:project => p) + end + create_gantt(p) + @gantt.render + assert_equal 6, @gantt.number_of_rows + assert !@gantt.truncated + create_gantt(p, :max_rows => 3) + @gantt.render + assert_equal 3, @gantt.number_of_rows + assert @gantt.truncated + end + + test "#number_of_rows_on_project should count 0 for an empty the project" do + create_gantt + assert_equal 0, @gantt.number_of_rows_on_project(@project) + end + + test "#number_of_rows_on_project should count the number of issues without a version" do + create_gantt + @project.issues << Issue.generate!(:project => @project, :fixed_version => nil) + assert_equal 2, @gantt.number_of_rows_on_project(@project) + end + + test "#number_of_rows_on_project should count the number of issues on versions, including cross-project" do + create_gantt + version = Version.generate! + @project.versions << version + @project.issues << Issue.generate!(:project => @project, :fixed_version => version) + assert_equal 3, @gantt.number_of_rows_on_project(@project) + end + + def setup_subjects + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none') + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 1), + :due_date => (today + 7)) + @project.issues << @issue + end + private :setup_subjects + + # TODO: more of an integration test + test "#subjects project should be rendered" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.project-name a", /#{@project.name}/ + end + + test "#subjects project should have an indent of 4" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.project-name[style*=left:4px]" + end + + test "#subjects version should be rendered" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.version-name a", /#{@version.name}/ + end + + test "#subjects version should be indented 24 (one level)" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.version-name[style*=left:24px]" + end + + test "#subjects version without assigned issues should not be rendered" do + setup_subjects + @version = Version.generate!(:effective_date => (today + 14), + :sharing => 'none', + :name => 'empty_version') + @project.versions << @version + @output_buffer = @gantt.subjects + assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0 + end + + test "#subjects issue should be rendered" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.issue-subject", /#{@issue.subject}/ + end + + test "#subjects issue should be indented 44 (two levels)" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.issue-subject[style*=left:44px]" + end + + test "#subjects issue assigned to a shared version of another project should be rendered" do + setup_subjects + p = Project.generate! + p.enabled_module_names = [:issue_tracking] + @shared_version = Version.generate!(:sharing => 'system') + p.versions << @shared_version + # Reassign the issue to a shared version of another project + @issue = Issue.generate!(:fixed_version => @shared_version, + :subject => "gantt#assigned_to_shared_version", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 1), + :due_date => (today + 7)) + @project.issues << @issue + @output_buffer = @gantt.subjects + assert_select "div.issue-subject", /#{@issue.subject}/ + end + + test "#subjects issue with subtasks should indent subtasks" do + setup_subjects + attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version} + @child1 = Issue.generate!( + attrs.merge(:subject => 'child1', + :parent_issue_id => @issue.id, + :start_date => (today - 1), + :due_date => (today + 2)) + ) + @child2 = Issue.generate!( + attrs.merge(:subject => 'child2', + :parent_issue_id => @issue.id, + :start_date => today, + :due_date => (today + 7)) + ) + @grandchild = Issue.generate!( + attrs.merge(:subject => 'grandchild', + :parent_issue_id => @child1.id, + :start_date => (today - 1), + :due_date => (today + 2)) + ) + @output_buffer = @gantt.subjects + # parent task 44px + assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/ + # children 64px + assert_select "div.issue-subject[style*=left:64px]", /child1/ + assert_select "div.issue-subject[style*=left:64px]", /child2/ + # grandchild 84px + assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer + end + + context "#lines" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7)) + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 1), + :due_date => (today + 7)) + @project.issues << @issue + @output_buffer = @gantt.lines + end + + context "project" do + should "be rendered" do + assert_select "div.project.task_todo" + assert_select "div.project.starting" + assert_select "div.project.ending" + assert_select "div.label.project", /#{@project.name}/ + end + end + + context "version" do + should "be rendered" do + assert_select "div.version.task_todo" + assert_select "div.version.starting" + assert_select "div.version.ending" + assert_select "div.label.version", /#{@version.name}/ + end + end + + context "issue" do + should "be rendered" do + assert_select "div.task_todo" + assert_select "div.task.label", /#{@issue.done_ratio}/ + assert_select "div.tooltip", /#{@issue.subject}/ + end + end + end + + context "#subject_for_project" do + setup do + create_gantt + end + + context ":html format" do + should "add an absolute positioned div" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the project name" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'div', :text => /#{@project.name}/ + end + + should "include a link to the project" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/ + end + + should "style overdue projects" do + @project.enabled_module_names = [:issue_tracking] + @project.versions << Version.generate!(:effective_date => (today - 1)) + assert @project.reload.overdue?, "Need an overdue project for this test" + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'div span.project-overdue' + end + end + end + + context "#line_for_project" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today - 1)) + @project.versions << @version + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 7), + :due_date => (today + 7)) + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_todo[style*=left:28px]", true, @output_buffer + end + + should "be the total width of the project" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_todo[style*=width:58px]", true, @output_buffer + end + end + + context "starting marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_from', today) + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.starting", false, @output_buffer + end + + should "appear at the starting point" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.starting[style*=left:28px]", true, @output_buffer + end + end + + context "ending marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.ending", false, @output_buffer + end + + should "appear at the end of the date range" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.ending[style*=left:88px]", true, @output_buffer + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.label", /#{@project.name}/ + end + + should "show the project name" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.label", /#{@project.name}/ + end + end + end + end + + context "#subject_for_version" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today - 1)) + @project.versions << @version + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#subject_for_version", + :tracker => @tracker, + :project => @project, + :start_date => today) + + end + + context ":html format" do + should "add an absolute positioned div" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the version name" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div', :text => /#{@version.name}/ + end + + should "include a link to the version" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'a[href=?]', Regexp.escape("/versions/#{@version.to_param}"), :text => /#{@version.name}/ + end + + should "style late versions" do + assert @version.overdue?, "Need an overdue version for this test" + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div span.version-behind-schedule' + end + + should "style behind schedule versions" do + assert @version.behind_schedule?, "Need a behind schedule version for this test" + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div span.version-behind-schedule' + end + end + end + + context "#line_for_version" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7)) + @project.versions << @version + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 7), + :due_date => (today + 7)) + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_todo[style*=left:28px]", true, @output_buffer + end + + should "be the total width of the version" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_todo[style*=width:58px]", true, @output_buffer + end + end + + context "late line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_late[style*=left:28px]", true, @output_buffer + end + + should "be the total delayed width of the version" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_late[style*=width:30px]", true, @output_buffer + end + end + + context "done line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_done[style*=left:28px]", true, @output_buffer + end + + should "be the total done width of the version" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_done[style*=width:16px]", true, @output_buffer + end + end + + context "starting marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_from', today) + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.starting", false + end + + should "appear at the starting point" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.starting[style*=left:28px]", true, @output_buffer + end + end + + context "ending marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.ending", false + end + + should "appear at the end of the date range" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.ending[style*=left:88px]", true, @output_buffer + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.label", /#{@version.name}/ + end + + should "show the version name" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.label", /#{@version.name}/ + end + + should "show the percent complete" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.label", /30%/ + end + end + end + end + + context "#subject_for_issue" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @issue = Issue.generate!(:subject => "gantt#subject_for_issue", + :tracker => @tracker, + :project => @project, + :start_date => (today - 3), + :due_date => (today - 1)) + @project.issues << @issue + end + + context ":html format" do + should "add an absolute positioned div" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the issue subject" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'div', :text => /#{@issue.subject}/ + end + + should "include a link to the issue" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/ + end + + should "style overdue issues" do + assert @issue.overdue?, "Need an overdue issue for this test" + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'div span.issue-overdue' + end + end + end + + context "#line_for_issue" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7)) + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 7), + :due_date => (today + 7)) + @project.issues << @issue + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_todo[style*=left:28px]", true, @output_buffer + end + + should "be the total width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_todo[style*=width:58px]", true, @output_buffer + end + end + + context "late line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_late[style*=left:28px]", true, @output_buffer + end + + should "be the total delayed width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_late[style*=width:30px]", true, @output_buffer + end + end + + context "done line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=left:28px]", true, @output_buffer + end + + should "be the total done width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + # 15 days * 4 px * 30% - 2 px for borders = 16 px + assert_select "div.task_done[style*=width:16px]", true, @output_buffer + end + + should "not be the total done width if the chart starts after issue start date" do + create_gantt(@project, :date_from => (today - 5)) + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=left:0px]", true, @output_buffer + assert_select "div.task_done[style*=width:8px]", true, @output_buffer + end + + context "for completed issue" do + setup do + @issue.done_ratio = 100 + end + + should "be the total width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=width:58px]", true, @output_buffer + end + + should "be the total width of the issue with due_date=start_date" do + @issue.due_date = @issue.start_date + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=width:2px]", true, @output_buffer + end + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task.label", true, @output_buffer + end + + should "show the issue status" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task.label", /#{@issue.status.name}/ + end + + should "show the percent complete" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task.label", /30%/ + end + end + end + + should "have an issue tooltip" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.tooltip", /#{@issue.subject}/ + end + end + + def test_sort_issues_no_date + project = Project.generate! + issue1 = Issue.generate!(:subject => "test", :project => project) + issue2 = Issue.generate!(:subject => "test", :project => project) + assert issue1.root_id < issue2.root_id + child1 = Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project) + child2 = Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project) + child3 = Issue.generate!(:parent_issue_id => child1.id, :subject => 'child', + :project => project) + assert_equal child1.root_id, child2.root_id + assert child1.lft < child2.lft + assert child3.lft < child2.lft + issues = [child3, child2, child1, issue2, issue1] + Redmine::Helpers::Gantt.sort_issues!(issues) + assert_equal [issue1.id, child1.id, child3.id, child2.id, issue2.id], + issues.map{|v| v.id} + end + + def test_sort_issues_root_only + project = Project.generate! + issue1 = Issue.generate!(:subject => "test", :project => project) + issue2 = Issue.generate!(:subject => "test", :project => project) + issue3 = Issue.generate!(:subject => "test", :project => project, + :start_date => (today - 1)) + issue4 = Issue.generate!(:subject => "test", :project => project, + :start_date => (today - 2)) + issues = [issue4, issue3, issue2, issue1] + Redmine::Helpers::Gantt.sort_issues!(issues) + assert_equal [issue1.id, issue2.id, issue4.id, issue3.id], + issues.map{|v| v.id} + end + + def test_sort_issues_tree + project = Project.generate! + issue1 = Issue.generate!(:subject => "test", :project => project) + issue2 = Issue.generate!(:subject => "test", :project => project, + :start_date => (today - 2)) + issue1_child1 = + Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project) + issue1_child2 = + Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project, :start_date => (today - 10)) + issue1_child1_child1 = + Issue.generate!(:parent_issue_id => issue1_child1.id, :subject => 'child', + :project => project, :start_date => (today - 8)) + issue1_child1_child2 = + Issue.generate!(:parent_issue_id => issue1_child1.id, :subject => 'child', + :project => project, :start_date => (today - 9)) + issue1_child1_child1_logic = Redmine::Helpers::Gantt.sort_issue_logic(issue1_child1_child1) + assert_equal [[today - 10, issue1.id], [today - 9, issue1_child1.id], + [today - 8, issue1_child1_child1.id]], + issue1_child1_child1_logic + issue1_child1_child2_logic = Redmine::Helpers::Gantt.sort_issue_logic(issue1_child1_child2) + assert_equal [[today - 10, issue1.id], [today - 9, issue1_child1.id], + [today - 9, issue1_child1_child2.id]], + issue1_child1_child2_logic + issues = [issue1_child1_child2, issue1_child1_child1, issue1_child2, + issue1_child1, issue2, issue1] + Redmine::Helpers::Gantt.sort_issues!(issues) + assert_equal [issue1.id, issue1_child1.id, issue1_child2.id, + issue1_child1_child2.id, issue1_child1_child1.id, issue2.id], + issues.map{|v| v.id} + end + + def test_sort_versions + project = Project.generate! + version1 = Version.create!(:project => project, :name => 'test1') + version2 = Version.create!(:project => project, :name => 'test2', :effective_date => '2013-10-25') + version3 = Version.create!(:project => project, :name => 'test3') + version4 = Version.create!(:project => project, :name => 'test4', :effective_date => '2013-10-02') + + assert_equal versions.sort, Redmine::Helpers::Gantt.sort_versions!(versions) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09db2139c5f23fc2a9c87d7a056e38c3f53388a9.svn-base --- a/.svn/pristine/09/09db2139c5f23fc2a9c87d7a056e38c3f53388a9.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1096 +0,0 @@ -# Estonian localization for Redmine -# Copyright (C) 2012 Kaitseministeerium -# -# 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. - -et: - # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) - direction: ltr - 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: "%d.%m.%Y" - short: "%d.%b" - long: "%d. %B %Y" - - day_names: [Pühapäev, Esmaspäev, Teisipäev, Kolmapäev, Neljapäev, Reede, Laupäev] - abbr_day_names: [P, E, T, K, N, R, L] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, Jaanuar, Veebruar, Märts, Aprill, Mai, Juuni, Juuli, August, September, Oktoober, November, Detsember] - abbr_month_names: [~, jaan, veebr, märts, apr, mai, juuni, juuli, aug, sept, okt, nov, dets] - # Used in date_select and datime_select. - order: - - :year - - :month - - :day - - time: - formats: - default: "%d.%m.%Y %H:%M" - time: "%H:%M" - short: "%d.%b %H:%M" - long: "%d. %B %Y %H:%M %z" - am: "enne lõunat" - pm: "peale lõunat" - - datetime: - distance_in_words: - half_a_minute: "pool minutit" - less_than_x_seconds: - one: "vähem kui sekund" - other: "vähem kui %{count} sekundit" - x_seconds: - one: "1 sekund" - other: "%{count} sekundit" - less_than_x_minutes: - one: "vähem kui minut" - other: "vähem kui %{count} minutit" - x_minutes: - one: "1 minut" - other: "%{count} minutit" - about_x_hours: - one: "umbes tund" - other: "umbes %{count} tundi" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "1 päev" - other: "%{count} päeva" - about_x_months: - one: "umbes kuu" - other: "umbes %{count} kuud" - x_months: - one: "1 kuu" - other: "%{count} kuud" - about_x_years: - one: "umbes aasta" - other: "umbes %{count} aastat" - over_x_years: - one: "rohkem kui aasta" - other: "rohkem kui %{count} aastat" - almost_x_years: - one: "peaaegu aasta" - other: "peaaegu %{count} aastat" - - number: - format: - separator: "." - delimiter: "" - precision: 3 - - human: - format: - delimiter: "" - precision: 3 - storage_units: - format: "%n %u" - units: - byte: - one: "bait" - other: "baiti" - kb: "KB" - mb: "MB" - gb: "GB" - tb: "TB" - -# Used in array.to_sentence. - support: - array: - sentence_connector: "ja" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "1 viga ei võimaldanud selle %{model} salvestamist" - other: "%{count} viga ei võimaldanud selle %{model} salvestamist" - messages: - inclusion: "ei ole nimekirjas" - exclusion: "on reserveeritud" - invalid: "ei sobi" - confirmation: "ei lange kinnitusega kokku" - accepted: "peab olema aktsepteeritud" - empty: "ei või olla tühi" - blank: "ei või olla täitmata" - too_long: "on liiga pikk (lubatud on kuni %{count} märki)" - too_short: "on liiga lühike (vaja on vähemalt %{count} märki)" - wrong_length: "on vale pikkusega (peaks olema %{count} märki)" - taken: "on juba võetud" - not_a_number: "ei ole arv" - not_a_date: "ei ole korrektne kuupäev" - greater_than: "peab olema suurem kui %{count}" - greater_than_or_equal_to: "peab olema võrdne või suurem kui %{count}" - equal_to: "peab võrduma %{count}-ga" - less_than: "peab olema väiksem kui %{count}" - less_than_or_equal_to: "peab olema võrdne või väiksem kui %{count}" - odd: "peab olema paaritu arv" - even: "peab olema paarisarv" - greater_than_start_date: "peab olema suurem kui alguskuupäev" - not_same_project: "ei kuulu sama projekti juurde" - circular_dependency: "See suhe looks vastastikuse sõltuvuse" - cant_link_an_issue_with_a_descendant: "Teemat ei saa sidustada tema enda alamteemaga" - - actionview_instancetag_blank_option: "Palun vali" - - general_text_No: "Ei" - general_text_Yes: "Jah" - general_text_no: "ei" - general_text_yes: "jah" - general_lang_name: "Eesti" - general_csv_separator: "," - general_csv_decimal_separator: "." - general_csv_encoding: ISO-8859-13 - general_pdf_encoding: UTF-8 - general_first_day_of_week: "1" - - notice_account_updated: "Konto uuendamine õnnestus." - notice_account_invalid_creditentials: "Sobimatu kasutajanimi või parool" - notice_account_password_updated: "Parooli uuendamine õnnestus." - notice_account_wrong_password: "Vale parool" - notice_account_register_done: "Konto loomine õnnestus. Konto aktiveerimiseks vajuta vastaval lingil Sulle saadetud e-kirjas." - notice_account_unknown_email: "Tundmatu kasutaja." - notice_can_t_change_password: "See konto kasutab välist autentimisallikat. Siin ei saa selle konto parooli vahetada." - notice_account_lost_email_sent: "Sulle saadeti e-kiri parooli vahetamise juhistega." - notice_account_activated: "Su konto on aktiveeritud. Saad nüüd sisse logida." - notice_successful_create: "Loomine õnnestus." - notice_successful_update: "Uuendamine õnnestus." - notice_successful_delete: "Kustutamine õnnestus." - notice_successful_connection: "Ühenduse loomine õnnestus." - notice_file_not_found: "Sellist lehte ei leitud." - notice_locking_conflict: "Teine kasutaja uuendas vahepeal neid andmeid." - notice_not_authorized: "Sul ei ole sellele lehele ligipääsuks õigusi." - notice_not_authorized_archived_project: "See projekt on arhiveeritud." - notice_email_sent: "%{value}-le saadeti kiri" - notice_email_error: "Kirja saatmisel tekkis viga (%{value})" - notice_feeds_access_key_reseted: "Sinu RSS juurdepääsuvõti nulliti." - notice_api_access_key_reseted: "Sinu API juurdepääsuvõti nulliti." - notice_failed_to_save_issues: "%{count} teemat %{total}-st ei õnnestunud salvestada: %{ids}." - notice_failed_to_save_time_entries: "%{count} ajakulu kannet %{total}-st ei õnnestunud salvestada: %{ids}." - notice_failed_to_save_members: "Liiget/liikmeid ei õnnestunud salvestada: %{errors}." - notice_no_issue_selected: "Ühtegi teemat ei ole valitud! Palun vali teema(d), mida soovid muuta." - notice_account_pending: "Sinu konto on loodud ja ootab nüüd administraatori kinnitust." - notice_default_data_loaded: "Algseadistuste laadimine õnnestus." - notice_unable_delete_version: "Versiooni kustutamine ei õnnestunud." - notice_unable_delete_time_entry: "Ajakulu kande kustutamine ei õnnestunud." - notice_issue_done_ratios_updated: "Teema edenemise astmed on uuendatud." - notice_gantt_chart_truncated: "Diagrammi kärbiti kuna ületati kuvatavate objektide suurim hulk (%{max})" - notice_issue_successful_create: "Teema %{id} loodud." - notice_issue_update_conflict: "Teine kasutaja uuendas seda teemat Sinuga samaaegselt." - notice_account_deleted: "Sinu konto on lõplikult kustutatud." - - error_can_t_load_default_data: "Algseadistusi ei saanud laadida: %{value}" - error_scm_not_found: "Seda sissekannet hoidlast ei leitud." - error_scm_command_failed: "Hoidla poole pöördumisel tekkis viga: %{value}" - error_scm_annotate: "Sissekannet ei eksisteeri või ei saa annoteerida." - error_scm_annotate_big_text_file: "Sissekannet ei saa annoteerida, kuna see on liiga pikk." - error_issue_not_found_in_project: "Teemat ei leitud või see ei kuulu siia projekti" - error_no_tracker_in_project: "Selle projektiga ei ole seostatud ühtegi valdkonda. Palun vaata üle projekti seaded." - error_no_default_issue_status: 'Teema algolek on määramata. Palun vaata asetused üle ("Seadistused -> Olekud").' - error_can_not_delete_custom_field: "Omaloodud välja kustutamine ei õnnestunud" - error_can_not_delete_tracker: "See valdkond on mõnes teemas kasutusel ja seda ei saa kustutada." - error_can_not_remove_role: "See roll on mõnes projektis kasutusel ja seda ei saa kustutada." - error_can_not_reopen_issue_on_closed_version: "Suletud versiooni juurde kuulunud teemat ei saa taasavada" - error_can_not_archive_project: "Seda projekti ei saa arhiveerida" - error_issue_done_ratios_not_updated: "Teema edenemise astmed jäid uuendamata." - error_workflow_copy_source: "Palun vali algne valdkond või roll" - error_workflow_copy_target: "Palun vali sihtvaldkon(na)d või -roll(id)" - error_unable_delete_issue_status: "Oleku kustutamine ei õnnestunud" - error_unable_to_connect: "Ühenduse loomine ei õnnestunud (%{value})" - error_attachment_too_big: "Faili ei saa üles laadida, sest see on lubatust (%{max_size}) pikem" - warning_attachments_not_saved: "%{count} faili salvestamine ei õnnestunud." - - mail_subject_lost_password: "Sinu %{value} parool" - mail_body_lost_password: "Et vahetada oma parooli, vajuta järgmisele lingile:" - mail_subject_register: "Sinu %{value} konto aktiveerimine" - mail_body_register: "Et aktiveerida oma kontot, vajuta järgmisele lingile:" - mail_body_account_information_external: "Sisse logimiseks saad kasutada oma %{value} kontot." - mail_body_account_information: "Sinu konto teave" - mail_subject_account_activation_request: "%{value} konto aktiveerimise nõue" - mail_body_account_activation_request: "Registreerus uus kasutaja (%{value}). Konto avamine ootab Sinu kinnitust:" - mail_subject_reminder: "%{count} teema tähtaeg jõuab kätte järgmise %{days} päeva jooksul" - mail_body_reminder: "%{count} Sulle määratud teema tähtaeg jõuab kätte järgmise %{days} päeva jooksul:" - mail_subject_wiki_content_added: "Lisati '%{id}' vikileht" - mail_body_wiki_content_added: "'%{id}' vikileht lisati %{author} poolt." - mail_subject_wiki_content_updated: "Uuendati '%{id}' vikilehte" - mail_body_wiki_content_updated: "'%{id}' vikilehte uuendati %{author} poolt." - - gui_validation_error: "1 viga" - gui_validation_error_plural: "%{count} viga" - - field_name: "Nimi" - field_description: "Kirjeldus" - field_summary: "Kokkuvõte" - field_is_required: "Kohustuslik" - field_firstname: "Eesnimi" - field_lastname: "Perekonnanimi" - field_mail: "E-post" - field_filename: "Fail" - field_filesize: "Pikkus" - field_downloads: "Allalaadimist" - field_author: "Autor" - field_created_on: "Loodud" - field_updated_on: "Uuendatud" - field_field_format: "Tüüp" - field_is_for_all: "Kõigile projektidele" - field_possible_values: "Võimalikud väärtused" - field_regexp: "Regulaarne avaldis" - field_min_length: "Vähim pikkus" - field_max_length: "Suurim pikkus" - field_value: "Väärtus" - field_category: "Kategooria" - field_title: "Pealkiri" - field_project: "Projekt" - field_issue: "Teema" - field_status: "Olek" - field_notes: "Märkused" - field_is_closed: "Sulgeb teema" - field_is_default: "Algolek" - field_tracker: "Valdkond" - field_subject: "Teema" - field_due_date: "Tähtaeg" - field_assigned_to: "Tegeleja" - field_priority: "Prioriteet" - field_fixed_version: "Sihtversioon" - field_user: "Kasutaja" - field_principal: "Vastutav isik" - field_role: "Roll" - field_homepage: "Koduleht" - field_is_public: "Avalik" - field_parent: "Emaprojekt" - field_is_in_roadmap: "Teemad on teekaardil näha" - field_login: "Kasutajanimi" - field_mail_notification: "Teated e-kirjaga" - field_admin: "Admin" - field_last_login_on: "Viimane ühendus" - field_language: "Keel" - field_effective_date: "Tähtaeg" - field_password: "Parool" - field_new_password: "Uus parool" - field_password_confirmation: "Kinnitus" - field_version: "Versioon" - field_type: "Tüüp" - field_host: "Server" - field_port: "Port" - field_account: "Konto" - field_base_dn: "Baas DN" - field_attr_login: "Kasutajanime atribuut" - field_attr_firstname: "Eesnime atribuut" - field_attr_lastname: "Perekonnanime atribuut" - field_attr_mail: "E-posti atribuut" - field_onthefly: "Kasutaja automaatne loomine" - field_start_date: "Alguskuupäev" - field_done_ratio: "% tehtud" - field_auth_source: "Autentimise viis" - field_hide_mail: "Peida e-posti aadress" - field_comments: "Kommentaar" - field_url: "URL" - field_start_page: "Esileht" - field_subproject: "Alamprojekt" - field_hours: "tundi" - field_activity: "Tegevus" - field_spent_on: "Kuupäev" - field_identifier: "Tunnus" - field_is_filter: "Kasutatakse filtrina" - field_issue_to: "Seotud teema" - field_delay: "Viivitus" - field_assignable: "Saab määrata teemadega tegelema" - field_redirect_existing_links: "Suuna olemasolevad lingid ringi" - field_estimated_hours: "Eeldatav ajakulu" - field_column_names: "Veerud" - field_time_entries: "Ajakulu" - field_time_zone: "Ajatsoon" - field_searchable: "Otsitav" - field_default_value: "Vaikimisi" - field_comments_sorting: "Kommentaaride järjestus" - field_parent_title: "Pärineb lehest" - field_editable: "Muudetav" - field_watcher: "Jälgija" - field_identity_url: "OpenID URL" - field_content: "Sisu" - field_group_by: "Grupeeri tulemus" - field_sharing: "Teemade jagamine" - field_parent_issue: "Pärineb teemast" - field_member_of_group: "Tegeleja grupp" - field_assigned_to_role: "Tegeleja roll" - field_text: "Tekstiväli" - field_visible: "Nähtav" - field_warn_on_leaving_unsaved: "Hoiata salvestamata sisuga lehtedelt lahkumisel" - field_issues_visibility: "See roll näeb" - field_is_private: "Privaatne" - field_commit_logs_encoding: "Sissekannete kodeering" - field_scm_path_encoding: "Teeraja märkide kodeering" - field_path_to_repository: "Hoidla teerada" - field_root_directory: "Juurkataloog" - field_cvsroot: "CVSROOT" - field_cvs_module: "Moodul" - field_repository_is_default: "Peamine hoidla" - field_multiple: "Korraga mitu väärtust" - field_auth_source_ldap_filter: "LDAP filter" - - setting_app_title: "Veebilehe pealkiri" - setting_app_subtitle: "Veebilehe alampealkiri" - setting_welcome_text: "Tervitustekst" - setting_default_language: "Vaikimisi keel" - setting_login_required: "Autentimine kohustuslik" - setting_self_registration: "Omaloodud konto aktiveerimine" - setting_attachment_max_size: "Manuse suurim pikkus" - setting_issues_export_limit: "Teemade ekspordi limiit" - setting_mail_from: "Saatja e-posti aadress" - setting_bcc_recipients: "Saajaid ei näidata (lähevad BCC reale)" - setting_plain_text_mail: "E-kiri tavalise tekstina (ilma HTML-ta)" - setting_host_name: "Serveri nimi ja teerada" - setting_text_formatting: "Vormindamise abi" - setting_wiki_compression: "Viki ajaloo pakkimine" - setting_feeds_limit: "Atom voogude suurim objektide arv" - setting_default_projects_public: "Uued projektid on vaikimisi avalikud" - setting_autofetch_changesets: "Lae uuendused automaatselt" - setting_sys_api_enabled: "Hoidlate haldamine veebiteenuse kaudu" - setting_commit_ref_keywords: "Viitade võtmesõnad" - setting_commit_fix_keywords: "Paranduste võtmesõnad" - setting_autologin: "Automaatne sisselogimine" - setting_date_format: "Kuupäevaformaat" - setting_time_format: "Ajaformaat" - setting_cross_project_issue_relations: "Luba siduda eri projektide teemasid" - setting_issue_list_default_columns: "Teemade nimekirja vaikimisi veerud" - setting_repositories_encodings: "Manuste ja hoidlate kodeering" - setting_emails_header: "E-kirja päis" - setting_emails_footer: "E-kirja jalus" - setting_protocol: "Protokoll" - setting_per_page_options: "Objekte lehe kohta variandid" - setting_user_format: "Kasutaja nime esitamise vorm" - setting_activity_days_default: "Projektide ajalugu näidatakse" - setting_display_subprojects_issues: "Näita projektis vaikimisi ka alamprojektide teemasid" - setting_enabled_scm: "Kasutatavad lähtekoodi haldusvahendid" - setting_mail_handler_body_delimiters: "Kärbi e-kirja lõpp peale sellist rida" - setting_mail_handler_api_enabled: "E-kirjade vastuvõtt veebiteenuse kaudu" - setting_mail_handler_api_key: "Veebiteenuse API võti" - setting_sequential_project_identifiers: "Genereeri järjestikused projektitunnused" - setting_gravatar_enabled: "Kasuta Gravatari kasutajaikoone" - setting_gravatar_default: "Vaikimisi kasutatav ikoon" - setting_diff_max_lines_displayed: "Enim korraga näidatavaid erinevusi" - setting_file_max_size_displayed: "Kuvatava tekstifaili suurim pikkus" - setting_repository_log_display_limit: "Enim ajaloos näidatavaid sissekandeid" - setting_openid: "Luba OpenID-ga registreerimine ja sisselogimine" - setting_password_min_length: "Lühima lubatud parooli pikkus" - setting_new_project_user_role_id: "Projekti looja roll oma projektis" - setting_default_projects_modules: "Vaikimisi moodulid uutes projektides" - setting_issue_done_ratio: "Määra teema edenemise aste" - setting_issue_done_ratio_issue_field: "kasutades vastavat välja" - setting_issue_done_ratio_issue_status: "kasutades teema olekut" - setting_start_of_week: "Nädala alguspäev" - setting_rest_api_enabled: "Luba REST API kasutamine" - setting_cache_formatted_text: "Puhverda vormindatud teksti" - setting_default_notification_option: "Vaikimisi teavitatakse" - setting_commit_logtime_enabled: "Luba ajakulu sisestamine" - setting_commit_logtime_activity_id: "Tegevus kulunud ajal" - setting_gantt_items_limit: "Gantti diagrammi objektide suurim hulk" - setting_issue_group_assignment: "Luba teemade andmine gruppidele" - setting_default_issue_start_date_to_creation_date: "Uute teemade alguskuupäevaks teema loomise päev" - setting_commit_cross_project_ref: "Luba viiteid ja parandusi ka kõigi teiste projektide teemadele" - setting_unsubscribe: "Luba kasutajal oma konto kustutada" - - permission_add_project: "Projekte luua" - permission_add_subprojects: "Alamprojekte luua" - permission_edit_project: "Projekte muuta" - permission_select_project_modules: "Projektimooduleid valida" - permission_manage_members: "Liikmeid hallata" - permission_manage_project_activities: "Projekti tegevusi hallata" - permission_manage_versions: "Versioone hallata" - permission_manage_categories: "Kategooriaid hallata" - permission_view_issues: "Teemasid näha" - permission_add_issues: "Teemasid lisada" - permission_edit_issues: "Teemasid uuendada" - permission_manage_issue_relations: "Teemade seoseid hallata" - permission_set_issues_private: "Teemasid avalikeks või privaatseiks seada" - permission_set_own_issues_private: "Omi teemasid avalikeks või privaatseiks seada" - permission_add_issue_notes: "Märkusi lisada" - permission_edit_issue_notes: "Märkusi muuta" - permission_edit_own_issue_notes: "Omi märkusi muuta" - permission_move_issues: "Teemasid teise projekti tõsta" - permission_delete_issues: "Teemasid kustutada" - permission_manage_public_queries: "Avalikke päringuid hallata" - permission_save_queries: "Päringuid salvestada" - permission_view_gantt: "Gantti diagramme näha" - permission_view_calendar: "Kalendrit näha" - permission_view_issue_watchers: "Jälgijate nimekirja näha" - permission_add_issue_watchers: "Jälgijaid lisada" - permission_delete_issue_watchers: "Jälgijaid kustutada" - permission_log_time: "Ajakulu sisestada" - permission_view_time_entries: "Ajakulu näha" - permission_edit_time_entries: "Ajakulu kandeid muuta" - permission_edit_own_time_entries: "Omi ajakulu kandeid muuta" - permission_manage_news: "Uudiseid hallata" - permission_comment_news: "Uudiseid kommenteerida" - permission_manage_documents: "Dokumente hallata" - permission_view_documents: "Dokumente näha" - permission_manage_files: "Faile hallata" - permission_view_files: "Faile näha" - permission_manage_wiki: "Vikit hallata" - permission_rename_wiki_pages: "Vikilehti ümber nimetada" - permission_delete_wiki_pages: "Vikilehti kustutada" - permission_view_wiki_pages: "Vikit näha" - permission_view_wiki_edits: "Viki ajalugu näha" - permission_edit_wiki_pages: "Vikilehti muuta" - permission_delete_wiki_pages_attachments: "Manuseid kustutada" - permission_protect_wiki_pages: "Vikilehti kaitsta" - permission_manage_repository: "Hoidlaid hallata" - permission_browse_repository: "Hoidlaid sirvida" - permission_view_changesets: "Sissekandeid näha" - permission_commit_access: "Sissekandeid teha" - permission_manage_boards: "Foorumeid hallata" - permission_view_messages: "Postitusi näha" - permission_add_messages: "Postitusi lisada" - permission_edit_messages: "Postitusi muuta" - permission_edit_own_messages: "Omi postitusi muuta" - permission_delete_messages: "Postitusi kustutada" - permission_delete_own_messages: "Omi postitusi kustutada" - permission_export_wiki_pages: "Vikilehti eksportida" - permission_manage_subtasks: "Alamteemasid hallata" - permission_manage_related_issues: "Seotud teemasid hallata" - - project_module_issue_tracking: "Teemade jälgimine" - project_module_time_tracking: "Ajakulu arvestus" - project_module_news: "Uudised" - project_module_documents: "Dokumendid" - project_module_files: "Failid" - project_module_wiki: "Viki" - project_module_repository: "Hoidlad" - project_module_boards: "Foorumid" - project_module_calendar: "Kalender" - project_module_gantt: "Gantt" - - label_user: "Kasutaja" - label_user_plural: "Kasutajad" - label_user_new: "Uus kasutaja" - label_user_anonymous: "Anonüümne" - label_project: "Projekt" - label_project_new: "Uus projekt" - label_project_plural: "Projektid" - label_x_projects: - zero: "pole projekte" - one: "1 projekt" - other: "%{count} projekti" - label_project_all: "Kõik projektid" - label_project_latest: "Viimased projektid" - label_issue: "Teema" - label_issue_new: "Uus teema" - label_issue_plural: "Teemad" - label_issue_view_all: "Teemade nimekiri" - label_issues_by: "Teemad %{value} järgi" - label_issue_added: "Teema lisatud" - label_issue_updated: "Teema uuendatud" - label_issue_note_added: "Märkus lisatud" - label_issue_status_updated: "Olek uuendatud" - label_issue_priority_updated: "Prioriteet uuendatud" - label_document: "Dokument" - label_document_new: "Uus dokument" - label_document_plural: "Dokumendid" - label_document_added: "Dokument lisatud" - label_role: "Roll" - label_role_plural: "Rollid" - label_role_new: "Uus roll" - label_role_and_permissions: "Rollid ja õigused" - label_role_anonymous: "Anonüümne" - label_role_non_member: "Mitteliige" - label_member: "Liige" - label_member_new: "Uus liige" - label_member_plural: "Liikmed" - label_tracker: "Valdkond" - label_tracker_plural: "Valdkonnad" - label_tracker_new: "Uus valdkond" - label_workflow: "Töövood" - label_issue_status: "Olek" - label_issue_status_plural: "Olekud" - label_issue_status_new: "Uus olek" - label_issue_category: "Kategooria" - label_issue_category_plural: "Kategooriad" - label_issue_category_new: "Uus kategooria" - label_custom_field: "Omaloodud väli" - label_custom_field_plural: "Omaloodud väljad" - label_custom_field_new: "Uus väli" - label_enumerations: "Loetelud" - label_enumeration_new: "Uus väärtus" - label_information: "Teave" - label_information_plural: "Teave" - label_please_login: "Palun logi sisse" - label_register: "Registreeru" - label_login_with_open_id_option: "või logi sisse OpenID-ga" - label_password_lost: "Kui parool on ununud..." - label_home: "Kodu" - label_my_page: "Oma leht" - label_my_account: "Oma konto" - label_my_projects: "Oma projektid" - label_my_page_block: "Uus blokk" - label_administration: "Seadistused" - label_login: "Logi sisse" - label_logout: "Logi välja" - label_help: "Abi" - label_reported_issues: "Minu poolt lisatud teemad" - label_assigned_to_me_issues: "Minu teha olevad teemad" - label_last_login: "Viimane ühendus" - label_registered_on: "Registreeritud" - label_activity: "Ajalugu" - label_overall_activity: "Üldine tegevuste ajalugu" - label_user_activity: "%{value} tegevuste ajalugu" - label_new: "Uus" - label_logged_as: "Sisse logitud kui" - label_environment: "Keskkond" - label_authentication: "Autentimine" - label_auth_source: "Autentimisallikas" - label_auth_source_new: "Uus autentimisallikas" - label_auth_source_plural: "Autentimisallikad" - label_subproject_plural: "Alamprojektid" - label_subproject_new: "Uus alamprojekt" - label_and_its_subprojects: "%{value} ja selle alamprojektid" - label_min_max_length: "Min.-maks. pikkus" - label_list: "Nimekiri" - label_date: "Kuupäev" - label_integer: "Täisarv" - label_float: "Ujukomaarv" - label_boolean: "Tõeväärtus" - label_string: "Tekst" - label_text: "Pikk tekst" - label_attribute: "Atribuut" - label_attribute_plural: "Atribuudid" - label_download: "%{count} allalaadimine" - label_download_plural: "%{count} allalaadimist" - label_no_data: "Pole" - label_change_status: "Muuda olekut" - label_history: "Ajalugu" - label_attachment: "Fail" - label_attachment_new: "Uus fail" - label_attachment_delete: "Kustuta fail" - label_attachment_plural: "Failid" - label_file_added: "Fail lisatud" - label_report: "Aruanne" - label_report_plural: "Aruanded" - label_news: "Uudised" - label_news_new: "Lisa uudis" - label_news_plural: "Uudised" - label_news_latest: "Viimased uudised" - label_news_view_all: "Kõik uudised" - label_news_added: "Uudis lisatud" - label_news_comment_added: "Kommentaar uudisele lisatud" - label_settings: "Seaded" - label_overview: "Ülevaade" - label_version: "Versioon" - label_version_new: "Uus versioon" - label_version_plural: "Versioonid" - label_close_versions: "Sulge lõpetatud versioonid" - label_confirmation: "Kinnitus" - label_export_to: "Samuti saadaval kujul:" - label_read: "Loe..." - label_public_projects: "Avalikud projektid" - label_open_issues: "avatud" - label_open_issues_plural: "avatud" - label_closed_issues: "suletud" - label_closed_issues_plural: "suletud" - label_x_open_issues_abbr_on_total: - zero: "0 avatud / %{total}" - one: "1 avatud / %{total}" - other: "%{count} avatud / %{total}" - label_x_open_issues_abbr: - zero: "0 avatud" - one: "1 avatud" - other: "%{count} avatud" - label_x_closed_issues_abbr: - zero: "0 suletud" - one: "1 suletud" - other: "%{count} suletud" - label_x_issues: - zero: "0 teemat" - one: "1 teema" - other: "%{count} teemat" - label_total: "Kokku" - label_permissions: "Õigused" - label_current_status: "Praegune olek" - label_new_statuses_allowed: "Uued lubatud olekud" - label_all: "kõik" - label_none: "pole" - label_nobody: "eikeegi" - label_next: "Järgmine" - label_previous: "Eelmine" - label_used_by: "Kasutab" - label_details: "Üksikasjad" - label_add_note: "Lisa märkus" - label_per_page: "Lehe kohta" - label_calendar: "Kalender" - label_months_from: "kuu kaugusel" - label_gantt: "Gantt" - label_internal: "Sisemine" - label_last_changes: "viimased %{count} muudatust" - label_change_view_all: "Kõik muudatused" - label_personalize_page: "Kujunda leht ümber" - label_comment: "Kommentaar" - label_comment_plural: "Kommentaarid" - label_x_comments: - zero: "kommentaare pole" - one: "1 kommentaar" - other: "%{count} kommentaari" - label_comment_add: "Lisa kommentaar" - label_comment_added: "Kommentaar lisatud" - label_comment_delete: "Kustuta kommentaar" - label_query: "Omaloodud päring" - label_query_plural: "Omaloodud päringud" - label_query_new: "Uus päring" - label_my_queries: "Mu omaloodud päringud" - label_filter_add: "Lisa filter" - label_filter_plural: "Filtrid" - label_equals: "on" - label_not_equals: "ei ole" - label_in_less_than: "on väiksem kui" - label_in_more_than: "on suurem kui" - label_greater_or_equal: "suurem-võrdne" - label_less_or_equal: "väiksem-võrdne" - label_between: "vahemikus" - label_in: "sisaldub hulgas" - label_today: "täna" - label_all_time: "piirideta" - label_yesterday: "eile" - label_this_week: "sel nädalal" - label_last_week: "eelmisel nädalal" - label_last_n_days: "viimase %{count} päeva jooksul" - label_this_month: "sel kuul" - label_last_month: "eelmisel kuul" - label_this_year: "sel aastal" - label_date_range: "Kuupäevavahemik" - label_less_than_ago: "uuem kui" - label_more_than_ago: "vanem kui" - label_ago: "vanus" - label_contains: "sisaldab" - label_not_contains: "ei sisalda" - label_day_plural: "päeva" - label_repository: "Hoidla" - label_repository_new: "Uus hoidla" - label_repository_plural: "Hoidlad" - label_browse: "Sirvi" - label_modification: "%{count} muudatus" - label_modification_plural: "%{count} muudatust" - label_branch: "Haru" - label_tag: "Sildiga" - label_revision: "Sissekanne" - label_revision_plural: "Sissekanded" - label_revision_id: "Sissekande kood %{value}" - label_associated_revisions: "Seotud sissekanded" - label_added: "lisatud" - label_modified: "muudetud" - label_copied: "kopeeritud" - label_renamed: "ümber nimetatud" - label_deleted: "kustutatud" - label_latest_revision: "Viimane sissekanne" - label_latest_revision_plural: "Viimased sissekanded" - label_view_revisions: "Haru ajalugu" - label_view_all_revisions: "Kogu ajalugu" - label_max_size: "Suurim pikkus" - label_sort_highest: "Nihuta esimeseks" - label_sort_higher: "Nihuta üles" - label_sort_lower: "Nihuta alla" - label_sort_lowest: "Nihuta viimaseks" - label_roadmap: "Teekaart" - label_roadmap_due_in: "Tähtaeg %{value}" - label_roadmap_overdue: "%{value} hiljaks jäänud" - label_roadmap_no_issues: "Selles versioonis ei ole teemasid" - label_search: "Otsi" - label_result_plural: "Tulemused" - label_all_words: "Kõik sõnad" - label_wiki: "Viki" - label_wiki_edit: "Viki muutmine" - label_wiki_edit_plural: "Viki muutmised" - label_wiki_page: "Vikileht" - label_wiki_page_plural: "Vikilehed" - label_index_by_title: "Järjesta pealkirja järgi" - label_index_by_date: "Järjesta kuupäeva järgi" - label_current_version: "Praegune versioon" - label_preview: "Eelvaade" - label_feed_plural: "Vood" - label_changes_details: "Kõigi muudatuste üksikasjad" - label_issue_tracking: "Teemade jälgimine" - label_spent_time: "Kulutatud aeg" - label_overall_spent_time: "Kokku kulutatud aeg" - label_f_hour: "%{value} tund" - label_f_hour_plural: "%{value} tundi" - label_time_tracking: "Ajakulu arvestus" - label_change_plural: "Muudatused" - label_statistics: "Statistika" - label_commits_per_month: "Sissekandeid kuu kohta" - label_commits_per_author: "Sissekandeid autori kohta" - label_diff: "erinevused" - label_view_diff: "Vaata erinevusi" - label_diff_inline: "teksti sees" - label_diff_side_by_side: "kõrvuti" - label_options: "Valikud" - label_copy_workflow_from: "Kopeeri see töövoog" - label_permissions_report: "Õiguste aruanne" - label_watched_issues: "Jälgitud teemad" - label_related_issues: "Seotud teemad" - label_applied_status: "Kehtestatud olek" - label_loading: "Laadimas..." - label_relation_new: "Uus seos" - label_relation_delete: "Kustuta seos" - label_relates_to: "seostub" - label_duplicates: "duplitseerib" - label_duplicated_by: "duplitseerija" - label_blocks: "blokeerib" - label_blocked_by: "blokeerija" - label_precedes: "eelneb" - label_follows: "järgneb" - label_end_to_start: "lõpust alguseni" - label_end_to_end: "lõpust lõpuni" - label_start_to_start: "algusest alguseni" - label_start_to_end: "algusest lõpuni" - label_stay_logged_in: "Püsi sisselogituna" - label_disabled: "pole võimalik" - label_show_completed_versions: "Näita lõpetatud versioone" - label_me: "mina" - label_board: "Foorum" - label_board_new: "Uus foorum" - label_board_plural: "Foorumid" - label_board_locked: "Lukus" - label_board_sticky: "Püsiteema" - label_topic_plural: "Teemad" - label_message_plural: "Postitused" - label_message_last: "Viimane postitus" - label_message_new: "Uus postitus" - label_message_posted: "Postitus lisatud" - label_reply_plural: "Vastused" - label_send_information: "Saada teave konto kasutajale" - label_year: "Aasta" - label_month: "Kuu" - label_week: "Nädal" - label_date_from: "Alates" - label_date_to: "Kuni" - label_language_based: "Kasutaja keele põhjal" - label_sort_by: "Sorteeri %{value} järgi" - label_send_test_email: "Saada kontrollkiri" - label_feeds_access_key: "RSS juurdepääsuvõti" - label_missing_feeds_access_key: "RSS juurdepääsuvõti on puudu" - label_feeds_access_key_created_on: "RSS juurdepääsuvõti loodi %{value} tagasi" - label_module_plural: "Moodulid" - label_added_time_by: "Lisatud %{author} poolt %{age} tagasi" - label_updated_time_by: "Uuendatud %{author} poolt %{age} tagasi" - label_updated_time: "Uuendatud %{value} tagasi" - label_jump_to_a_project: "Ava projekt..." - label_file_plural: "Failid" - label_changeset_plural: "Muudatused" - label_default_columns: "Vaikimisi veerud" - label_no_change_option: "(Ei muutu)" - label_bulk_edit_selected_issues: "Muuda valitud teemasid korraga" - label_bulk_edit_selected_time_entries: "Muuda valitud ajakandeid korraga" - label_theme: "Visuaalne teema" - label_default: "Tavaline" - label_search_titles_only: "Ainult pealkirjadest" - label_user_mail_option_all: "Kõigist tegevustest kõigis mu projektides" - label_user_mail_option_selected: "Kõigist tegevustest ainult valitud projektides..." - label_user_mail_option_none: "Teavitusi ei saadeta" - label_user_mail_option_only_my_events: "Ainult mu jälgitavatest või minuga seotud tegevustest" - label_user_mail_option_only_assigned: "Ainult minu teha olevate asjade kohta" - label_user_mail_option_only_owner: "Ainult mu oma asjade kohta" - label_user_mail_no_self_notified: "Ära teavita mind mu enda tehtud muudatustest" - label_registration_activation_by_email: "e-kirjaga" - label_registration_manual_activation: "käsitsi" - label_registration_automatic_activation: "automaatselt" - label_display_per_page: "Lehe kohta: %{value}" - label_age: "Vanus" - label_change_properties: "Muuda omadusi" - label_general: "Üldine" - label_more: "Rohkem" - label_scm: "Lähtekoodi haldusvahend" - label_plugins: "Lisamoodulid" - label_ldap_authentication: "LDAP autentimine" - label_downloads_abbr: "A/L" - label_optional_description: "Teave" - label_add_another_file: "Lisa veel üks fail" - label_preferences: "Eelistused" - label_chronological_order: "kronoloogiline" - label_reverse_chronological_order: "tagurpidi kronoloogiline" - label_planning: "Planeerimine" - label_incoming_emails: "Sissetulevad e-kirjad" - label_generate_key: "Genereeri võti" - label_issue_watchers: "Jälgijad" - label_example: "Näide" - label_display: "Kujundus" - label_sort: "Sorteeri" - label_ascending: "Kasvavalt" - label_descending: "Kahanevalt" - label_date_from_to: "Alates %{start} kuni %{end}" - label_wiki_content_added: "Vikileht lisatud" - label_wiki_content_updated: "Vikileht uuendatud" - label_group: "Grupp" - label_group_plural: "Grupid" - label_group_new: "Uus grupp" - label_time_entry_plural: "Kulutatud aeg" - label_version_sharing_none: "ei toimu" - label_version_sharing_descendants: "alamprojektidega" - label_version_sharing_hierarchy: "projektihierarhiaga" - label_version_sharing_tree: "projektipuuga" - label_version_sharing_system: "kõigi projektidega" - label_update_issue_done_ratios: "Uuenda edenemise astmeid" - label_copy_source: "Allikas" - label_copy_target: "Sihtkoht" - label_copy_same_as_target: "Sama mis sihtkoht" - label_display_used_statuses_only: "Näita ainult selles valdkonnas kasutusel olekuid" - label_api_access_key: "API juurdepääsuvõti" - label_missing_api_access_key: "API juurdepääsuvõti on puudu" - label_api_access_key_created_on: "API juurdepääsuvõti loodi %{value} tagasi" - label_profile: "Profiil" - label_subtask_plural: "Alamteemad" - label_project_copy_notifications: "Saada projekti kopeerimise kohta teavituskiri" - label_principal_search: "Otsi kasutajat või gruppi:" - label_user_search: "Otsi kasutajat:" - label_additional_workflow_transitions_for_author: "Luba ka järgmisi üleminekuid kui kasutaja on teema looja" - label_additional_workflow_transitions_for_assignee: "Luba ka järgmisi üleminekuid kui kasutaja on teemaga tegeleja" - label_issues_visibility_all: "kõiki teemasid" - label_issues_visibility_public: "kõiki mitteprivaatseid teemasid" - label_issues_visibility_own: "enda poolt loodud või enda teha teemasid" - label_git_report_last_commit: "Viimase sissekande teave otse failinimekirja" - label_parent_revision: "Eellane" - label_child_revision: "Järglane" - label_export_options: "%{export_format} ekspordivalikud" - label_copy_attachments: "Kopeeri manused" - label_item_position: "%{position}/%{count}" - label_completed_versions: "Lõpetatud versioonid" - label_search_for_watchers: "Otsi lisamiseks jälgijaid" - - button_login: "Logi sisse" - button_submit: "Sisesta" - button_save: "Salvesta" - button_check_all: "Märgi kõik" - button_uncheck_all: "Nulli valik" - button_collapse_all: "Voldi kõik kokku" - button_expand_all: "Voldi kõik lahti" - button_delete: "Kustuta" - button_create: "Loo" - button_create_and_continue: "Loo ja jätka" - button_test: "Testi" - button_edit: "Muuda" - button_edit_associated_wikipage: "Muuda seotud vikilehte: %{page_title}" - button_add: "Lisa" - button_change: "Muuda" - button_apply: "Lae" - button_clear: "Puhasta" - button_lock: "Lukusta" - button_unlock: "Ava lukust" - button_download: "Lae alla" - button_list: "Listi" - button_view: "Vaata" - button_move: "Tõsta" - button_move_and_follow: "Tõsta ja järgne" - button_back: "Tagasi" - button_cancel: "Katkesta" - button_activate: "Aktiveeri" - button_sort: "Sorteeri" - button_log_time: "Ajakulu" - button_rollback: "Rulli tagasi sellesse versiooni" - button_watch: "Jälgi" - button_unwatch: "Ära jälgi" - button_reply: "Vasta" - button_archive: "Arhiveeri" - button_unarchive: "Arhiivist tagasi" - button_reset: "Nulli" - button_rename: "Nimeta ümber" - button_change_password: "Vaheta parool" - button_copy: "Kopeeri" - button_copy_and_follow: "Kopeeri ja järgne" - button_annotate: "Annoteeri" - button_update: "Muuda" - button_configure: "Konfigureeri" - button_quote: "Tsiteeri" - button_duplicate: "Duplitseeri" - button_show: "Näita" - button_edit_section: "Muuda seda sektsiooni" - button_export: "Ekspordi" - button_delete_my_account: "Kustuta oma konto" - - status_active: "aktiivne" - status_registered: "registreeritud" - status_locked: "lukus" - - version_status_open: "avatud" - version_status_locked: "lukus" - version_status_closed: "suletud" - - field_active: "Aktiivne" - - text_select_mail_notifications: "Tegevused, millest peaks e-kirjaga teavitama" - text_regexp_info: "nt. ^[A-Z0-9]+$" - text_min_max_length_info: "0 tähendab, et piiranguid ei ole" - text_project_destroy_confirmation: "Oled Sa kindel oma soovis see projekt täielikult kustutada?" - text_subprojects_destroy_warning: "Alamprojekt(id) - %{value} - kustutatakse samuti." - text_workflow_edit: "Töövoo muutmiseks vali roll ja valdkond" - text_are_you_sure: "Oled Sa kindel?" - text_journal_changed: "%{label} muudetud %{old} -> %{new}" - text_journal_changed_no_detail: "%{label} uuendatud" - text_journal_set_to: "%{label} uus väärtus on %{value}" - text_journal_deleted: "%{label} kustutatud (%{old})" - text_journal_added: "%{label} %{value} lisatud" - text_tip_issue_begin_day: "teema avamise päev" - text_tip_issue_end_day: "teema sulgemise päev" - text_tip_issue_begin_end_day: "teema avati ja sulgeti samal päeval" - text_project_identifier_info: "Lubatud on ainult väikesed tähed (a-z), numbrid ja kriipsud.
    Peale salvestamist ei saa tunnust enam muuta." - text_caracters_maximum: "%{count} märki kõige rohkem." - text_caracters_minimum: "Peab olema vähemalt %{count} märki pikk." - text_length_between: "Pikkus %{min} kuni %{max} märki." - text_tracker_no_workflow: "Selle valdkonna jaoks ei ole ühtegi töövoogu kirjeldatud" - text_unallowed_characters: "Lubamatud märgid" - text_comma_separated: "Lubatud erinevad väärtused (komaga eraldatult)." - text_line_separated: "Lubatud erinevad väärtused (igaüks eraldi real)." - text_issues_ref_in_commit_messages: "Teemadele ja parandustele viitamine sissekannete märkustes" - text_issue_added: "%{author} lisas uue teema %{id}." - text_issue_updated: "%{author} uuendas teemat %{id}." - text_wiki_destroy_confirmation: "Oled Sa kindel oma soovis kustutada see Viki koos kogu sisuga?" - text_issue_category_destroy_question: "Kustutatavat kategooriat kasutab %{count} teema(t). Mis Sa soovid nendega ette võtta?" - text_issue_category_destroy_assignments: "Jäta teemadel kategooria määramata" - text_issue_category_reassign_to: "Määra teemad teise kategooriasse" - text_user_mail_option: "Valimata projektidest saad teavitusi ainult jälgitavate või Sinuga seotud asjade kohta (nt. Sinu loodud või teha teemad)." - text_no_configuration_data: "Rollid, valdkonnad, olekud ja töövood ei ole veel seadistatud.\nVäga soovitav on laadida vaikeasetused. Peale laadimist saad neid ise muuta." - text_load_default_configuration: "Lae vaikeasetused" - text_status_changed_by_changeset: "Kehtestatakse muudatuses %{value}." - text_time_logged_by_changeset: "Kehtestatakse muudatuses %{value}." - text_issues_destroy_confirmation: "Oled Sa kindel oma soovis valitud teema(d) kustutada?" - text_issues_destroy_descendants_confirmation: "See kustutab samuti %{count} alamteemat." - text_time_entries_destroy_confirmation: "Oled Sa kindel oma soovis valitud ajakulu kanne/kanded kustutada?" - text_select_project_modules: "Projektis kasutatavad moodulid" - text_default_administrator_account_changed: "Algne administraatori konto on muudetud" - text_file_repository_writable: "Manuste kataloog on kirjutatav" - text_plugin_assets_writable: "Lisamoodulite abifailide kataloog on kirjutatav" - text_rmagick_available: "RMagick on kasutatav (mittekohustuslik)" - text_destroy_time_entries_question: "Kustutatavatele teemadele oli kirja pandud %{hours} tundi. Mis Sa soovid ette võtta?" - text_destroy_time_entries: "Kustuta need tunnid" - text_assign_time_entries_to_project: "Vii tunnid üle teise projekti" - text_reassign_time_entries: "Määra tunnid sellele teemale:" - text_user_wrote: "%{value} kirjutas:" - text_enumeration_destroy_question: "Selle väärtusega on seotud %{count} objekt(i)." - text_enumeration_category_reassign_to: "Seo nad teise väärtuse külge:" - text_email_delivery_not_configured: "E-kirjade saatmine ei ole seadistatud ja teavitusi ei saadeta.\nKonfigureeri oma SMTP server failis config/configuration.yml ja taaskäivita Redmine." - text_repository_usernames_mapping: "Seosta Redmine kasutaja hoidlasse sissekannete tegijaga.\nSama nime või e-postiga kasutajad seostatakse automaatselt." - text_diff_truncated: "... Osa erinevusi jäi välja, sest neid on näitamiseks liiga palju." - text_custom_field_possible_values_info: "Üks rida iga väärtuse jaoks" - text_wiki_page_destroy_question: "Sel lehel on %{descendants} järglasleht(e) ja järeltulija(t). Mis Sa soovid ette võtta?" - text_wiki_page_nullify_children: "Muuda järglaslehed uuteks juurlehtedeks" - text_wiki_page_destroy_children: "Kustuta järglaslehed ja kõik nende järglased" - text_wiki_page_reassign_children: "Määra järglaslehed teise lehe külge" - text_own_membership_delete_confirmation: "Sa võtad endalt ära osa või kõik õigused ega saa edaspidi seda projekti võib-olla enam muuta.\nOled Sa jätkamises kindel?" - text_zoom_in: "Vaata lähemalt" - text_zoom_out: "Vaata kaugemalt" - text_warn_on_leaving_unsaved: "Sel lehel on salvestamata teksti, mis läheb kaduma, kui siit lehelt lahkud." - text_scm_path_encoding_note: "Vaikimisi UTF-8" - text_git_repository_note: "Hoidla peab olema paljas (bare) ja kohalik (nt. /gitrepo, c:\\gitrepo)" - text_mercurial_repository_note: "Hoidla peab olema kohalik (nt. /hgrepo, c:\\hgrepo)" - text_scm_command: "Hoidla poole pöördumise käsk" - text_scm_command_version: "Versioon" - text_scm_config: "Hoidlate poole pöördumist saab konfigureerida failis config/configuration.yml. Peale selle muutmist taaskäivita Redmine." - text_scm_command_not_available: "Hoidla poole pöördumine ebaõnnestus. Palun kontrolli seadistusi." - text_issue_conflict_resolution_overwrite: "Kehtesta oma muudatused (kõik märkused jäävad, ent muu võidakse üle kirjutada)" - text_issue_conflict_resolution_add_notes: "Lisa oma märkused, aga loobu teistest muudatustest" - text_issue_conflict_resolution_cancel: "Loobu kõigist muudatustest ja lae %{link} uuesti" - text_account_destroy_confirmation: "Oled Sa kindel?\nSu konto kustutatakse jäädavalt ja seda pole võimalik taastada." - - default_role_manager: "Haldaja" - default_role_developer: "Arendaja" - default_role_reporter: "Edastaja" - default_tracker_bug: "Veaparandus" - default_tracker_feature: "Täiendus" - default_tracker_support: "Klienditugi" - default_issue_status_new: "Avatud" - default_issue_status_in_progress: "Töös" - default_issue_status_resolved: "Lahendatud" - default_issue_status_feedback: "Tagasiside" - default_issue_status_closed: "Suletud" - default_issue_status_rejected: "Tagasi lükatud" - default_doc_category_user: "Juhend lõppkasutajale" - default_doc_category_tech: "Tehniline dokumentatsioon" - default_priority_low: "Aega on" - default_priority_normal: "Tavaline" - default_priority_high: "Pakiline" - default_priority_urgent: "Täna vaja" - default_priority_immediate: "Kohe vaja" - default_activity_design: "Kavandamine" - default_activity_development: "Arendamine" - - enumeration_issue_priorities: "Teemade prioriteedid" - enumeration_doc_categories: "Dokumentide kategooriad" - enumeration_activities: "Tegevused (ajakulu)" - enumeration_system_activity: "Süsteemi aktiivsus" - description_filter: "Filter" - description_search: "Otsinguväli" - description_choose_project: "Projektid" - description_project_scope: "Otsingu ulatus" - description_notes: "Märkused" - description_message_content: "Postituse sisu" - description_query_sort_criteria_attribute: "Sorteerimise kriteerium" - description_query_sort_criteria_direction: "Sorteerimise suund" - description_user_mail_notification: "E-kirjaga teavitamise seaded" - description_available_columns: "Kasutatavad veerud" - description_selected_columns: "Valitud veerud" - description_all_columns: "Kõik veerud" - description_issue_category_reassign: "Vali uus kategooria" - description_wiki_subpages_reassign: "Vali lehele uus vanem" - description_date_range_list: "Vali vahemik nimekirjast" - description_date_range_interval: "Vali vahemik algus- ja lõpukuupäeva abil" - description_date_from: "Sisesta alguskuupäev" - description_date_to: "Sisesta lõpukuupäev" - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: "Lubatud on ainult väikesed tähed (a-z), numbrid ja kriipsud.
    Peale salvestamist ei saa tunnust enam muuta." - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: "kõik" - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: "alamprojektidega" - label_cross_project_tree: "projektipuuga" - label_cross_project_hierarchy: "projektihierarhiaga" - label_cross_project_system: "kõigi projektidega" - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09ea8a2e3065e6d0cfba8be5ebe32c3e0767f60e.svn-base --- a/.svn/pristine/09/09ea8a2e3065e6d0cfba8be5ebe32c3e0767f60e.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Activity - - mattr_accessor :available_event_types, :default_event_types, :providers - - @@available_event_types = [] - @@default_event_types = [] - @@providers = Hash.new {|h,k| h[k]=[] } - - class << self - def map(&block) - yield self - end - - # Registers an activity provider - def register(event_type, options={}) - options.assert_valid_keys(:class_name, :default) - - event_type = event_type.to_s - providers = options[:class_name] || event_type.classify - providers = ([] << providers) unless providers.is_a?(Array) - - @@available_event_types << event_type unless @@available_event_types.include?(event_type) - @@default_event_types << event_type unless options[:default] == false - @@providers[event_type] += providers - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/09/09ee4c362357d89f421e143b7a604276903929a0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09ee4c362357d89f421e143b7a604276903929a0.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,94 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::PaginationTest < ActiveSupport::TestCase + + def setup + @klass = Redmine::Pagination::Paginator + end + + def test_count_is_zero + p = @klass.new 0, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + %w(first_page previous_page next_page last_page).each do |method| + assert_nil p.send(method), "#{method} was not nil" + end + assert_equal 0, p.first_item + assert_equal 0, p.last_item + assert_equal [], p.linked_pages + end + + def test_count_is_less_than_per_page + p = @klass.new 7, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_nil p.next_page + assert_equal 1, p.last_page + assert_equal 1, p.first_item + assert_equal 7, p.last_item + assert_equal [], p.linked_pages + end + + def test_count_is_equal_to_per_page + p = @klass.new 10, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_nil p.next_page + assert_equal 1, p.last_page + assert_equal 1, p.first_item + assert_equal 10, p.last_item + assert_equal [], p.linked_pages + end + + def test_2_pages + p = @klass.new 16, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_equal 2, p.next_page + assert_equal 2, p.last_page + assert_equal 1, p.first_item + assert_equal 10, p.last_item + assert_equal [1, 2], p.linked_pages + end + + def test_many_pages + p = @klass.new 155, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_equal 2, p.next_page + assert_equal 16, p.last_page + assert_equal 1, p.first_item + assert_equal 10, p.last_item + assert_equal [1, 2, 3, 16], p.linked_pages + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0a22e26c056016673b5cd0813b12a72fc4e8cff7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0a22e26c056016673b5cd0813b12a72fc4e8cff7.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,145 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueRelationsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :issue_relations, + :enabled_modules, + :enumerations, + :trackers, + :projects_trackers + + def setup + User.current = nil + @request.session[:user_id] = 3 + end + + def test_create + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 1, relation.issue_from_id + assert_equal 2, relation.issue_to_id + assert_equal 'relates', relation.relation_type + end + + def test_create_xhr + assert_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => 3, :relation => {:issue_to_id => '1', :relation_type => 'relates', :delay => ''} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 3, relation.issue_from_id + assert_equal 1, relation.issue_to_id + + assert_match /Bug #1/, response.body + end + + def test_create_should_accept_id_with_hash + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '#2', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_to_id + end + + def test_create_should_strip_id + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => ' 2 ', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_to_id + end + + def test_create_should_not_break_with_non_numerical_id + assert_no_difference 'IssueRelation.count' do + assert_nothing_raised do + post :create, :issue_id => 1, + :relation => {:issue_to_id => 'foo', :relation_type => 'relates', :delay => ''} + end + end + end + + def test_create_follows_relation_should_update_relations_list + issue1 = Issue.generate!(:subject => 'Followed issue', :start_date => Date.yesterday, :due_date => Date.today) + issue2 = Issue.generate! + + assert_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => issue2.id, + :relation => {:issue_to_id => issue1.id, :relation_type => 'follows', :delay => ''} + end + assert_match /Followed issue/, response.body + end + + def test_should_create_relations_with_visible_issues_only + Setting.cross_project_issue_relations = '1' + assert_nil Issue.visible(User.find(3)).find_by_id(4) + + assert_no_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''} + end + end + + def test_create_xhr_with_failure + assert_no_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => 3, :relation => {:issue_to_id => '999', :relation_type => 'relates', :delay => ''} + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + + assert_match /errorExplanation/, response.body + end + + def test_destroy + assert_difference 'IssueRelation.count', -1 do + delete :destroy, :id => '2' + end + end + + def test_destroy_xhr + IssueRelation.create!(:relation_type => IssueRelation::TYPE_RELATES) do |r| + r.issue_from_id = 3 + r.issue_to_id = 1 + end + + assert_difference 'IssueRelation.count', -1 do + xhr :delete, :destroy, :id => '2' + + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + assert_match /relation-2/, response.body + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0a4b990585c7f952b85cc821c139a4f90df28825.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0a4b990585c7f952b85cc821c139a4f90df28825.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,113 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 TimelogCustomFieldsVisibilityTest < ActionController::TestCase + tests TimelogController + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issue_statuses, + :trackers, + :projects_trackers, + :enabled_modules, + :enumerations, + :workflows + + def setup + field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all} + @fields = [] + @fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true))) + @fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2]))) + @fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3]))) + @issue = Issue.generate!( + :author_id => 1, + :project_id => 1, + :tracker_id => 1, + :custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'} + ) + TimeEntry.generate!(:issue => @issue) + + @user_with_role_on_other_project = User.generate! + User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3)) + + @users_to_test = { + User.find(1) => [@field1, @field2, @field3], + User.find(3) => [@field1, @field2], + @user_with_role_on_other_project => [@field1], # should see field1 only on Project 1 + User.generate! => [@field1], + User.anonymous => [@field1] + } + + Member.where(:project_id => 1).each do |member| + member.destroy unless @users_to_test.keys.include?(member.principal) + end + end + + def test_index_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :index, :project_id => 1, :issue_id => @issue.id, :c => (['hours'] + @fields.map{|f| "issue.cf_#{f.id}"}) + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}" + else + assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}" + end + end + end + end + + def test_index_as_csv_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :index, :project_id => 1, :issue_id => @issue.id, :c => (['hours'] + @fields.map{|f| "issue.cf_#{f.id}"}), :format => 'csv' + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_include "Value#{i}", response.body, "User #{user.id} was not able to view #{field.name} in CSV" + else + assert_not_include "Value#{i}", response.body, "User #{user.id} was able to view #{field.name} in CSV" + end + end + end + end + + def test_index_with_partial_custom_field_visibility_should_show_visible_custom_fields_only + Issue.delete_all + TimeEntry.delete_all + p1 = Project.generate! + p2 = Project.generate! + user = User.generate! + User.add_to_project(user, p1, Role.find_all_by_id(1,3)) + User.add_to_project(user, p2, Role.find_all_by_id(3)) + TimeEntry.generate!(:issue => Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueA'})) + TimeEntry.generate!(:issue => Issue.generate!(:project => p2, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueB'})) + TimeEntry.generate!(:issue => Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueC'})) + + @request.session[:user_id] = user.id + get :index, :c => ["hours", "issue.cf_#{@field2.id}"] + assert_select 'td', :text => 'ValueA' + assert_select 'td', :text => 'ValueB', :count => 0 + assert_select 'td', :text => 'ValueC' + + get :index, :set_filter => '1', "issue.cf_#{@field2.id}" => '*' + assert_equal %w(ValueA ValueC), assigns(:entries).map{|i| i.issue.custom_field_value(@field2)}.sort + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0a53e228ad2ca76e975fdb76cc443721ba07788d.svn-base --- a/.svn/pristine/0a/0a53e228ad2ca76e975fdb76cc443721ba07788d.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,86 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Group < Principal - include Redmine::SafeAttributes - - has_and_belongs_to_many :users, :after_add => :user_added, - :after_remove => :user_removed - - acts_as_customizable - - validates_presence_of :lastname - validates_uniqueness_of :lastname, :case_sensitive => false - validates_length_of :lastname, :maximum => 30 - - before_destroy :remove_references_before_destroy - - scope :sorted, order("#{table_name}.lastname ASC") - - safe_attributes 'name', - 'user_ids', - 'custom_field_values', - 'custom_fields', - :if => lambda {|group, user| user.admin?} - - def to_s - lastname.to_s - end - - def name - lastname - end - - def name=(arg) - self.lastname = arg - end - - def user_added(user) - members.each do |member| - next if member.project.nil? - user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id) - member.member_roles.each do |member_role| - user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id) - end - user_member.save! - end - end - - def user_removed(user) - members.each do |member| - MemberRole.find(:all, :include => :member, - :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy) - end - end - - def self.human_attribute_name(attribute_key_name, *args) - attr_name = attribute_key_name.to_s - if attr_name == 'lastname' - attr_name = "name" - end - super(attr_name, *args) - end - - private - - # Removes references that are not handled by associations - def remove_references_before_destroy - return if self.id.nil? - - Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0a7193067757b43aa4a973fedb71a5a6507643df.svn-base --- a/.svn/pristine/0a/0a7193067757b43aa4a973fedb71a5a6507643df.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,103 +0,0 @@ ---- -changesets_001: - commit_date: 2007-04-11 - committed_on: 2007-04-11 15:14:44 +02:00 - revision: 1 - id: 100 - comments: My very first commit - repository_id: 10 - committer: dlopper - user_id: 3 -changesets_002: - commit_date: 2007-04-12 - committed_on: 2007-04-12 15:14:44 +02:00 - revision: 2 - id: 101 - comments: 'This commit fixes #1, #2 and references #1 & #3' - repository_id: 10 - committer: dlopper - user_id: 3 -changesets_003: - commit_date: 2007-04-12 - committed_on: 2007-04-12 15:14:44 +02:00 - revision: 3 - id: 102 - comments: |- - A commit with wrong issue ids - IssueID #666 #3 - repository_id: 10 - committer: dlopper - user_id: 3 -changesets_004: - commit_date: 2007-04-12 - committed_on: 2007-04-12 15:14:44 +02:00 - revision: 4 - id: 103 - comments: |- - A commit with an issue id of an other project - IssueID 4 2 - repository_id: 10 - committer: dlopper - user_id: 3 -changesets_005: - commit_date: "2007-09-10" - comments: Modified one file in the folder. - committed_on: 2007-09-10 19:01:08 - revision: "5" - id: 104 - scmid: - user_id: 3 - repository_id: 10 - committer: dlopper -changesets_006: - commit_date: "2007-09-10" - comments: Moved helloworld.rb from / to /folder. - committed_on: 2007-09-10 19:01:47 - revision: "6" - id: 105 - scmid: - user_id: 3 - repository_id: 10 - committer: dlopper -changesets_007: - commit_date: "2007-09-10" - comments: Removed one file. - committed_on: 2007-09-10 19:02:16 - revision: "7" - id: 106 - scmid: - user_id: 3 - repository_id: 10 - committer: dlopper -changesets_008: - commit_date: "2007-09-10" - comments: |- - This commits references an issue. - Refs #2 - committed_on: 2007-09-10 19:04:35 - revision: "8" - id: 107 - scmid: - user_id: 3 - repository_id: 10 - committer: dlopper -changesets_009: - commit_date: "2009-09-10" - comments: One file added. - committed_on: 2009-09-10 19:04:35 - revision: "9" - id: 108 - scmid: - user_id: 3 - repository_id: 10 - committer: dlopper -changesets_010: - commit_date: "2009-09-10" - comments: Same file modified. - committed_on: 2009-09-10 19:04:35 - revision: "10" - id: 109 - scmid: - user_id: 3 - repository_id: 10 - committer: dlopper diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0ab473e135d07b597fe923abcf5c9582a5bc4830.svn-base Binary file .svn/pristine/0a/0ab473e135d07b597fe923abcf5c9582a5bc4830.svn-base has changed diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0ab85a80b71d453bc34359193f9b3a077fde14bb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0ab85a80b71d453bc34359193f9b3a077fde14bb.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,279 @@ +module ActiveRecord + module Acts #:nodoc: + module List #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list. + # The class that has this specified needs to have a +position+ column defined as an integer on + # the mapped database table. + # + # Todo list example: + # + # class TodoList < ActiveRecord::Base + # has_many :todo_items, :order => "position" + # end + # + # class TodoItem < ActiveRecord::Base + # belongs_to :todo_list + # acts_as_list :scope => :todo_list + # end + # + # todo_list.first.move_to_bottom + # todo_list.last.move_higher + module ClassMethods + # Configuration options are: + # + # * +column+ - specifies the column name to use for keeping the position integer (default: +position+) + # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id + # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible + # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. + # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' + def acts_as_list(options = {}) + configuration = { :column => "position", :scope => "1 = 1" } + configuration.update(options) if options.is_a?(Hash) + + configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ + + if configuration[:scope].is_a?(Symbol) + scope_condition_method = %( + def scope_condition + if #{configuration[:scope].to_s}.nil? + "#{configuration[:scope].to_s} IS NULL" + else + "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" + end + end + ) + else + scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" + end + + class_eval <<-EOV + include ActiveRecord::Acts::List::InstanceMethods + + def acts_as_list_class + ::#{self.name} + end + + def position_column + '#{configuration[:column]}' + end + + #{scope_condition_method} + + before_destroy :remove_from_list + before_create :add_to_list_bottom + EOV + end + end + + # All the methods available to a record that has had acts_as_list specified. Each method works + # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter + # lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is + # the first in the list of all chapters. + module InstanceMethods + # Insert the item at the given position (defaults to the top position of 1). + def insert_at(position = 1) + insert_at_position(position) + end + + # Swap positions with the next lower item, if one exists. + def move_lower + return unless lower_item + + acts_as_list_class.transaction do + lower_item.decrement_position + increment_position + end + end + + # Swap positions with the next higher item, if one exists. + def move_higher + return unless higher_item + + acts_as_list_class.transaction do + higher_item.increment_position + decrement_position + end + end + + # Move to the bottom of the list. If the item is already in the list, the items below it have their + # position adjusted accordingly. + def move_to_bottom + return unless in_list? + acts_as_list_class.transaction do + decrement_positions_on_lower_items + assume_bottom_position + end + end + + # Move to the top of the list. If the item is already in the list, the items above it have their + # position adjusted accordingly. + def move_to_top + return unless in_list? + acts_as_list_class.transaction do + increment_positions_on_higher_items + assume_top_position + end + end + + # Move to the given position + def move_to=(pos) + case pos.to_s + when 'highest' + move_to_top + when 'higher' + move_higher + when 'lower' + move_lower + when 'lowest' + move_to_bottom + end + reset_positions_in_list + end + + def reset_positions_in_list + acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i| + unless item.send(position_column) == (i + 1) + acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id}) + end + end + end + + # Removes the item from the list. + def remove_from_list + if in_list? + decrement_positions_on_lower_items + update_attribute position_column, nil + end + end + + # Increase the position of this item without adjusting the rest of the list. + def increment_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i + 1 + end + + # Decrease the position of this item without adjusting the rest of the list. + def decrement_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i - 1 + end + + # Return +true+ if this object is the first in the list. + def first? + return false unless in_list? + self.send(position_column) == 1 + end + + # Return +true+ if this object is the last in the list. + def last? + return false unless in_list? + self.send(position_column) == bottom_position_in_list + end + + # Return the next higher item in the list. + def higher_item + return nil unless in_list? + acts_as_list_class.where( + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" + ).first + end + + # Return the next lower item in the list. + def lower_item + return nil unless in_list? + acts_as_list_class.where( + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" + ).first + end + + # Test if this record is in a list + def in_list? + !send(position_column).nil? + end + + private + def add_to_list_top + increment_positions_on_all_items + end + + def add_to_list_bottom + self[position_column] = bottom_position_in_list.to_i + 1 + end + + # Overwrite this method to define the scope of the list changes + def scope_condition() "1" end + + # Returns the bottom position number in the list. + # bottom_position_in_list # => 2 + def bottom_position_in_list(except = nil) + item = bottom_item(except) + item ? item.send(position_column) : 0 + end + + # Returns the bottom item + def bottom_item(except = nil) + conditions = scope_condition + conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except + acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first + end + + # Forces item to assume the bottom position in the list. + def assume_bottom_position + update_attribute(position_column, bottom_position_in_list(self).to_i + 1) + end + + # Forces item to assume the top position in the list. + def assume_top_position + update_attribute(position_column, 1) + end + + # This has the effect of moving all the higher items up one. + def decrement_positions_on_higher_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}" + ) + end + + # This has the effect of moving all the lower items up one. + def decrement_positions_on_lower_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the higher items down one. + def increment_positions_on_higher_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the lower items down one. + def increment_positions_on_lower_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" + ) + end + + # Increments position (position_column) of all items in the list. + def increment_positions_on_all_items + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" + ) + end + + def insert_at_position(position) + remove_from_list + increment_positions_on_lower_items(position) + self.update_attribute(position_column, position) + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0ac45c1808f5ee15f2487069dcbe64385e65e79f.svn-base --- a/.svn/pristine/0a/0ac45c1808f5ee15f2487069dcbe64385e65e79f.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Tracker < ActiveRecord::Base - - CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze - # Fields that can be disabled - # Other (future) fields should be appended, not inserted! - CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze - CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze - - before_destroy :check_integrity - has_many :issues - has_many :workflow_rules, :dependent => :delete_all do - def copy(source_tracker) - WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) - end - end - - has_and_belongs_to_many :projects - has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' - acts_as_list - - attr_protected :field_bits - - validates_presence_of :name - validates_uniqueness_of :name - validates_length_of :name, :maximum => 30 - - scope :sorted, order("#{table_name}.position ASC") - scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} - - def to_s; name end - - def <=>(tracker) - position <=> tracker.position - end - - # Returns an array of IssueStatus that are used - # in the tracker's workflows - def issue_statuses - if @issue_statuses - return @issue_statuses - elsif new_record? - return [] - end - - ids = WorkflowTransition. - connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). - flatten. - uniq - - @issue_statuses = IssueStatus.find_all_by_id(ids).sort - end - - def disabled_core_fields - i = -1 - @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0} - end - - def core_fields - CORE_FIELDS - disabled_core_fields - end - - def core_fields=(fields) - raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array) - - bits = 0 - CORE_FIELDS.each_with_index do |field, i| - unless fields.include?(field) - bits |= 2 ** i - end - end - self.fields_bits = bits - @disabled_core_fields = nil - core_fields - end - - # Returns the fields that are disabled for all the given trackers - def self.disabled_core_fields(trackers) - if trackers.present? - trackers.uniq.map(&:disabled_core_fields).reduce(:&) - else - [] - end - end - - # Returns the fields that are enabled for one tracker at least - def self.core_fields(trackers) - if trackers.present? - trackers.uniq.map(&:core_fields).reduce(:|) - else - CORE_FIELDS.dup - end - end - -private - def check_integrity - raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any? - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0ad2fa6a29d7061cdbc80b5e79aaef4886193bb6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0ad2fa6a29d7061cdbc80b5e79aaef4886193bb6.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'zlib' + +class WikiContent < ActiveRecord::Base + self.locking_column = 'version' + belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + validates_presence_of :text + validates_length_of :comments, :maximum => 255, :allow_nil => true + + acts_as_versioned + + after_save :send_notification + + def visible?(user=User.current) + page.visible?(user) + end + + def project + page.project + end + + def attachments + page.nil? ? [] : page.attachments + end + + # Returns the mail adresses of users that should be notified + def recipients + notified = project.notified_users + notified.reject! {|user| !visible?(user)} + notified.collect(&:mail) + end + + # Return true if the content is the current page content + def current_version? + true + end + + class Version + belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id' + belongs_to :author, :class_name => '::User', :foreign_key => 'author_id' + attr_protected :data + + acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"}, + :description => :comments, + :datetime => :updated_on, + :type => 'wiki-page', + :group => :page, + :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}} + + acts_as_activity_provider :type => 'wiki_edits', + :timestamp => "#{WikiContent.versioned_table_name}.updated_on", + :author_key => "#{WikiContent.versioned_table_name}.author_id", + :permission => :view_wiki_edits, + :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + + "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + + "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + + "#{WikiContent.versioned_table_name}.id", + :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + + "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + + "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"} + + after_destroy :page_update_after_destroy + + def text=(plain) + case Setting.wiki_compression + when 'gzip' + begin + self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION) + self.compression = 'gzip' + rescue + self.data = plain + self.compression = '' + end + else + self.data = plain + self.compression = '' + end + plain + end + + def text + @text ||= begin + str = case compression + when 'gzip' + Zlib::Inflate.inflate(data) + else + # uncompressed data + data + end + str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) + str + end + end + + def project + page.project + end + + # Return true if the content is the current page content + def current_version? + page.content.version == self.version + end + + # Returns the previous version or nil + def previous + @previous ||= WikiContent::Version. + reorder('version DESC'). + includes(:author). + where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first + end + + # Returns the next version or nil + def next + @next ||= WikiContent::Version. + reorder('version ASC'). + includes(:author). + where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first + end + + private + + # Updates page's content if the latest version is removed + # or destroys the page if it was the only version + def page_update_after_destroy + latest = page.content.versions.reorder("#{self.class.table_name}.version DESC").first + if latest && page.content.version != latest.version + raise ActiveRecord::Rollback unless page.content.revert_to!(latest) + elsif latest.nil? + raise ActiveRecord::Rollback unless page.destroy + end + end + end + + private + + def send_notification + # new_record? returns false in after_save callbacks + if id_changed? + if Setting.notified_events.include?('wiki_content_added') + Mailer.wiki_content_added(self).deliver + end + elsif text_changed? + if Setting.notified_events.include?('wiki_content_updated') + Mailer.wiki_content_updated(self).deliver + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0aeabf349bacf0ce65e0c6107e082ed00e2f7277.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0aeabf349bacf0ce65e0c6107e082ed00e2f7277.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,79 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 Watcher < ActiveRecord::Base + belongs_to :watchable, :polymorphic => true + belongs_to :user + + validates_presence_of :user + validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] + validate :validate_user + + # Returns true if at least one object among objects is watched by user + def self.any_watched?(objects, user) + objects = objects.reject(&:new_record?) + if objects.any? + objects.group_by {|object| object.class.base_class}.each do |base_class, objects| + if Watcher.where(:watchable_type => base_class.name, :watchable_id => objects.map(&:id), :user_id => user.id).exists? + return true + end + end + end + false + end + + # Unwatch things that users are no longer allowed to view + def self.prune(options={}) + if options.has_key?(:user) + prune_single_user(options[:user], options) + else + pruned = 0 + User.where("id IN (SELECT DISTINCT user_id FROM #{table_name})").all.each do |user| + pruned += prune_single_user(user, options) + end + pruned + end + end + + protected + + def validate_user + errors.add :user_id, :invalid unless user.nil? || user.active? + end + + private + + def self.prune_single_user(user, options={}) + return unless user.is_a?(User) + pruned = 0 + where(:user_id => user.id).all.each do |watcher| + next if watcher.watchable.nil? + + if options.has_key?(:project) + next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project] + end + + if watcher.watchable.respond_to?(:visible?) + unless watcher.watchable.visible?(user) + watcher.destroy + pruned += 1 + end + end + end + pruned + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0a/0afcb9f11cbb636f2ad064a5db4a409c777ec8da.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0afcb9f11cbb636f2ad064a5db4a409c777ec8da.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,8 @@ +class PopulateUsersType < ActiveRecord::Migration + def self.up + Principal.where("type IS NULL").update_all("type = 'User'") + end + + def self.down + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b13f339ca5a8485aed4100def04ec2f702c2643.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b13f339ca5a8485aed4100def04ec2f702c2643.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,107 @@ +source 'https://rubygems.org' + +gem "rails", "3.2.19" +gem "rake", "~> 10.1.1" +gem "jquery-rails", "~> 2.0.2" +gem "coderay", "~> 1.1.0" +gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] +gem "builder", "3.0.0" +gem "mime-types" +gem "awesome_nested_set", "2.1.6" + +# Optional gem for LDAP authentication +group :ldap do + gem "net-ldap", "~> 0.3.1" +end + +# Optional gem for OpenID authentication +group :openid do + gem "ruby-openid", "~> 2.3.0", :require => "openid" + gem "rack-openid" +end + +platforms :mri, :mingw do + # Optional gem for exporting the gantt to a PNG file, not supported with jruby + group :rmagick do + # RMagick 2 supports ruby 1.9 + # RMagick 1 would be fine for ruby 1.8 but Bundler does not support + # different requirements for the same gem on different platforms + gem "rmagick", ">= 2.0.0" + end + + # Optional Markdown support, not for JRuby + group :markdown do + # TODO: upgrade to redcarpet 3.x when ruby1.8 support is dropped + gem "redcarpet", "~> 2.3.0" + end +end + +platforms :jruby do + # jruby-openssl is bundled with JRuby 1.7.0 + gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0' + gem "activerecord-jdbc-adapter", "~> 1.3.2" +end + +# Include database gems for the adapters found in the database +# configuration file +require 'erb' +require 'yaml' +database_file = File.join(File.dirname(__FILE__), "config/database.yml") +if File.exist?(database_file) + database_config = YAML::load(ERB.new(IO.read(database_file)).result) + adapters = database_config.values.map {|c| c['adapter']}.compact.uniq + if adapters.any? + adapters.each do |adapter| + case adapter + when 'mysql2' + gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw] + gem "activerecord-jdbcmysql-adapter", :platforms => :jruby + when 'mysql' + gem "mysql", "~> 2.8.1", :platforms => [:mri, :mingw] + gem "activerecord-jdbcmysql-adapter", :platforms => :jruby + when /postgresql/ + gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw] + gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby + when /sqlite3/ + gem "sqlite3", :platforms => [:mri, :mingw] + gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby + when /sqlserver/ + gem "tiny_tds", "~> 0.5.1", :platforms => [:mri, :mingw] + gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw] + else + warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems") + end + end + else + warn("No adapter found in config/database.yml, please configure it first") + end +else + warn("Please configure your config/database.yml first") +end + +group :development do + gem "rdoc", ">= 2.4.2" + gem "yard" +end + +group :test do + gem "shoulda", "~> 3.3.2" + gem "mocha", "~> 1.0.0", :require => 'mocha/api' + if RUBY_VERSION >= '1.9.3' + gem "capybara", "~> 2.1.0" + gem "selenium-webdriver" + end +end + +local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") +if File.exists?(local_gemfile) + puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(local_gemfile) +end + +# Load plugins' Gemfiles +Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file| + puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` + #TODO: switch to "eval_gemfile file" when bundler >= 1.2.0 will be required (rails 4) + instance_eval File.read(file), file +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b1931e7310d3bdeabe0a2c171beeaecda67ff41.svn-base --- a/.svn/pristine/0b/0b1931e7310d3bdeabe0a2c171beeaecda67ff41.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Platform - class << self - def mswin? - (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || - (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b26e69e5ae731de14e1272a803c5ca8472b3a28.svn-base --- a/.svn/pristine/0b/0b26e69e5ae731de14e1272a803c5ca8472b3a28.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -class AddRepositoriesPermissions < ActiveRecord::Migration - # model removed - class Permission < ActiveRecord::Base; end - - def self.up - Permission.create :controller => "repositories", :action => "show", :description => "button_view", :sort => 1450, :is_public => true - Permission.create :controller => "repositories", :action => "browse", :description => "label_browse", :sort => 1460, :is_public => true - Permission.create :controller => "repositories", :action => "entry", :description => "entry", :sort => 1462, :is_public => true - Permission.create :controller => "repositories", :action => "revisions", :description => "label_view_revisions", :sort => 1470, :is_public => true - Permission.create :controller => "repositories", :action => "revision", :description => "label_view_revisions", :sort => 1472, :is_public => true - Permission.create :controller => "repositories", :action => "diff", :description => "diff", :sort => 1480, :is_public => true - end - - def self.down - Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'show']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'browse']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'entry']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revisions']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revision']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'diff']).destroy - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b2cfb5e453d4c5b12b6d8b9ca9e37acb99e7715.svn-base --- a/.svn/pristine/0b/0b2cfb5e453d4c5b12b6d8b9ca9e37acb99e7715.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingDocumentsTest < ActionController::IntegrationTest - def test_documents_scoped_under_project - assert_routing( - { :method => 'get', :path => "/projects/567/documents" }, - { :controller => 'documents', :action => 'index', :project_id => '567' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/documents/new" }, - { :controller => 'documents', :action => 'new', :project_id => '567' } - ) - assert_routing( - { :method => 'post', :path => "/projects/567/documents" }, - { :controller => 'documents', :action => 'create', :project_id => '567' } - ) - end - - def test_documents - assert_routing( - { :method => 'get', :path => "/documents/22" }, - { :controller => 'documents', :action => 'show', :id => '22' } - ) - assert_routing( - { :method => 'get', :path => "/documents/22/edit" }, - { :controller => 'documents', :action => 'edit', :id => '22' } - ) - assert_routing( - { :method => 'put', :path => "/documents/22" }, - { :controller => 'documents', :action => 'update', :id => '22' } - ) - assert_routing( - { :method => 'delete', :path => "/documents/22" }, - { :controller => 'documents', :action => 'destroy', :id => '22' } - ) - assert_routing( - { :method => 'post', :path => "/documents/22/add_attachment" }, - { :controller => 'documents', :action => 'add_attachment', :id => '22' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b8467ea6271bd6bbf31748ec5f34d49b8671c8b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b8467ea6271bd6bbf31748ec5f34d49b8671c8b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,23 @@ +/* Azerbaijani (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Jamil Najafov (necefov33@gmail.com). */ +jQuery(function($) { + $.datepicker.regional['az'] = { + closeText: 'Bağla', + prevText: '<Geri', + nextText: 'İrəli>', + currentText: 'Bugün', + monthNames: ['Yanvar','Fevral','Mart','Aprel','May','İyun', + 'İyul','Avqust','Sentyabr','Oktyabr','Noyabr','Dekabr'], + monthNamesShort: ['Yan','Fev','Mar','Apr','May','İyun', + 'İyul','Avq','Sen','Okt','Noy','Dek'], + dayNames: ['Bazar','Bazar ertəsi','Çərşənbə axşamı','Çərşənbə','Cümə axşamı','Cümə','Şənbə'], + dayNamesShort: ['B','Be','Ça','Ç','Ca','C','Ş'], + dayNamesMin: ['B','B','Ç','С','Ç','C','Ş'], + weekHeader: 'Hf', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['az']); +}); diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b8a89ac0b31721906c0bc4fded1743272e7b058.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b8a89ac0b31721906c0bc4fded1743272e7b058.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,114 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'redmine/scm/adapters/darcs_adapter' + +class Repository::Darcs < Repository + validates_presence_of :url, :log_encoding + + 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::DarcsAdapter + end + + def self.scm_name + 'Darcs' + end + + def supports_directory_revisions? + true + end + + def entry(path=nil, identifier=nil) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.entry(path, patch.nil? ? nil : patch.scmid) + end + + def entries(path=nil, identifier=nil) + patch = nil + if ! identifier.nil? + patch = changesets.find_by_revision(identifier) + return nil if patch.nil? + end + entries = scm.entries(path, patch.nil? ? nil : patch.scmid) + if entries + entries.each do |entry| + # Search the DB for the entry's last change + if entry.lastrev && !entry.lastrev.scmid.blank? + changeset = changesets.find_by_scmid(entry.lastrev.scmid) + end + if changeset + entry.lastrev.identifier = changeset.revision + entry.lastrev.name = changeset.revision + entry.lastrev.time = changeset.committed_on + entry.lastrev.author = changeset.committer + end + end + end + load_entries_changesets(entries) + entries + end + + def cat(path, identifier=nil) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s) + scm.cat(path, patch.nil? ? nil : patch.scmid) + end + + def diff(path, rev, rev_to) + patch_from = changesets.find_by_revision(rev) + return nil if patch_from.nil? + patch_to = changesets.find_by_revision(rev_to) if rev_to + if path.blank? + path = patch_from.filechanges.collect{|change| change.path}.join(' ') + end + patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil + end + + def fetch_changesets + scm_info = scm.info + if scm_info + db_last_id = latest_changeset ? latest_changeset.scmid : nil + next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1 + # latest revision in the repository + scm_revision = scm_info.lastrev.scmid + unless changesets.find_by_scmid(scm_revision) + revisions = scm.revisions('', db_last_id, nil, :with_path => true) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => next_rev, + :scmid => revision.scmid, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + revision.paths.each do |change| + changeset.create_change(change) + end + next_rev += 1 + end if revisions + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0b99b1cce55da066eba41dff8f2bfa290c20b955.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b99b1cce55da066eba41dff8f2bfa290c20b955.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,71 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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/setup' + + 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 d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0bca8036fe4c38a6a73e7d14a9b6dbe36da41436.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0bca8036fe4c38a6a73e7d14a9b6dbe36da41436.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,12 @@ +changeset = 'This template must be used with --debug option\n' +changeset_quiet = 'This template must be used with --debug option\n' +changeset_verbose = 'This template must be used with --debug option\n' +changeset_debug = '\n{author|escape}\n{date|isodatesec}\n\n{file_mods}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n\n{parents}\n\n\n' + +file_mod = '{file_mod|urlescape}\n' +file_add = '{file_add|urlescape}\n' +file_del = '{file_del|urlescape}\n' +file_copy = '{name|urlescape}\n' +parent = '{node}\n' +header='\n\n\n' +# footer="" diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0bec89d5c0dbedaa5bc5e8099c531dc64fe9abe2.svn-base --- a/.svn/pristine/0b/0bec89d5c0dbedaa5bc5e8099c531dc64fe9abe2.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -<%= l(:text_issue_added, :id => "##{@issue.id}", :author => h(@issue.author)) %> -
    -<%= render :partial => 'issue', :formats => [:html], :locals => { :issue => @issue, :issue_url => @issue_url } %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0b/0bf8f19f5486edff5718369b36db7c358c6ec343.svn-base --- a/.svn/pristine/0b/0bf8f19f5486edff5718369b36db7c358c6ec343.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -<%= labelled_fields_for :issue, @issue do |f| %> - -
    -
    -<% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %> -

    <%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true}, - :onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %>

    - -<% else %> -

    <%= h(@issue.status.name) %>

    -<% end %> - -<% if @issue.safe_attribute? 'priority_id' %> -

    <%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %>

    -<% end %> - -<% if @issue.safe_attribute? 'assigned_to_id' %> -

    <%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %>

    -<% end %> - -<% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %> -

    <%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %> -<%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'), - new_project_issue_category_path(@issue.project), - :remote => true, - :method => 'get', - :title => l(:label_issue_category_new), - :tabindex => 200) if User.current.allowed_to?(:manage_categories, @issue.project) %>

    -<% end %> - -<% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %> -

    <%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %> -<%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'), - new_project_version_path(@issue.project), - :remote => true, - :method => 'get', - :title => l(:label_version_new), - :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %> -

    -<% end %> -
    - -
    -<% if @issue.safe_attribute? 'parent_issue_id' %> -

    <%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %>

    -<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path}')" %> -<% end %> - -<% if @issue.safe_attribute? 'start_date' %> -

    <%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('start_date') %><%= calendar_for('issue_start_date') if @issue.leaf? %>

    -<% end %> - -<% if @issue.safe_attribute? 'due_date' %> -

    <%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('due_date') %><%= calendar_for('issue_due_date') if @issue.leaf? %>

    -<% end %> - -<% if @issue.safe_attribute? 'estimated_hours' %> -

    <%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %>

    -<% end %> - -<% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %> -

    <%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %>

    -<% end %> -
    -
    - -<% if @issue.safe_attribute? 'custom_field_values' %> -<%= render :partial => 'issues/form_custom_fields' %> -<% end %> - -<% end %> - -<% include_calendar_headers_tags %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0c006adf29e0abdbaa9644cee502d6882d303828.svn-base --- a/.svn/pristine/0c/0c006adf29e0abdbaa9644cee502d6882d303828.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 MimeType - - MIME_TYPES = { - 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade', - 'text/css' => 'css', - 'text/html' => 'html,htm,xhtml', - 'text/jsp' => 'jsp', - 'text/x-c' => 'c,cpp,cc,h,hh', - 'text/x-csharp' => 'cs', - 'text/x-java' => 'java', - 'text/x-html-template' => 'rhtml', - 'text/x-perl' => 'pl,pm', - 'text/x-php' => 'php,php3,php4,php5', - 'text/x-python' => 'py', - 'text/x-ruby' => 'rb,rbw,ruby,rake,erb', - 'text/x-csh' => 'csh', - 'text/x-sh' => 'sh', - 'text/xml' => 'xml,xsd,mxml', - 'text/yaml' => 'yml,yaml', - 'text/csv' => 'csv', - 'text/x-po' => 'po', - 'image/gif' => 'gif', - 'image/jpeg' => 'jpg,jpeg,jpe', - 'image/png' => 'png', - 'image/tiff' => 'tiff,tif', - 'image/x-ms-bmp' => 'bmp', - 'image/x-xpixmap' => 'xpm', - 'image/svg+xml'=> 'svg', - 'application/javascript' => 'js', - 'application/pdf' => 'pdf', - 'application/rtf' => 'rtf', - 'application/msword' => 'doc', - 'application/vnd.ms-excel' => 'xls', - 'application/vnd.ms-powerpoint' => 'ppt,pps', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx', - 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', - 'application/vnd.oasis.opendocument.text' => 'odt', - 'application/vnd.oasis.opendocument.presentation' => 'odp', - 'application/x-7z-compressed' => '7z', - 'application/x-rar-compressed' => 'rar', - 'application/x-tar' => 'tar', - 'application/zip' => 'zip', - 'application/x-gzip' => 'gz', - }.freeze - - EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)| - exts.split(',').each {|ext| map[ext.strip] = type} - map - end - - # returns mime type for name or nil if unknown - def self.of(name) - return nil unless name - m = name.to_s.match(/(^|\.)([^\.]+)$/) - EXTENSIONS[m[2].downcase] if m - end - - # Returns the css class associated to - # the mime type of name - def self.css_class_of(name) - mime = of(name) - mime && mime.gsub('/', '-') - end - - def self.main_mimetype_of(name) - mimetype = of(name) - mimetype.split('/').first if mimetype - end - - # return true if mime-type for name is type/* - # otherwise false - def self.is_type?(type, name) - main_mimetype = main_mimetype_of(name) - type.to_s == main_mimetype - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0c1341a53592418e4a2dd8783234ed2bf47a5fde.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c1341a53592418e4a2dd8783234ed2bf47a5fde.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1 @@ +$('#principals_for_new_member').html('<%= escape_javascript(render_principals_for_new_members(@project)) %>'); diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0c20b053aa02a18ebfa242903b54c5d2423345a1.svn-base --- a/.svn/pristine/0c/0c20b053aa02a18ebfa242903b54c5d2423345a1.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingRolesTest < ActionController::IntegrationTest - def test_roles - assert_routing( - { :method => 'get', :path => "/roles" }, - { :controller => 'roles', :action => 'index' } - ) - assert_routing( - { :method => 'get', :path => "/roles.xml" }, - { :controller => 'roles', :action => 'index', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/roles/2.xml" }, - { :controller => 'roles', :action => 'show', :id => '2', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/roles/new" }, - { :controller => 'roles', :action => 'new' } - ) - assert_routing( - { :method => 'post', :path => "/roles" }, - { :controller => 'roles', :action => 'create' } - ) - assert_routing( - { :method => 'get', :path => "/roles/2/edit" }, - { :controller => 'roles', :action => 'edit', :id => '2' } - ) - assert_routing( - { :method => 'put', :path => "/roles/2" }, - { :controller => 'roles', :action => 'update', :id => '2' } - ) - assert_routing( - { :method => 'delete', :path => "/roles/2" }, - { :controller => 'roles', :action => 'destroy', :id => '2' } - ) - ["get", "post"].each do |method| - assert_routing( - { :method => method, :path => "/roles/permissions" }, - { :controller => 'roles', :action => 'permissions' } - ) - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0c52d5a82094593c338acef265e5dd3106b35b1d.svn-base --- a/.svn/pristine/0c/0c52d5a82094593c338acef265e5dd3106b35b1d.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,68 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ApplicationTest < ActionController::IntegrationTest - include Redmine::I18n - - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows - - def test_set_localization - Setting.default_language = 'en' - - # a french user - get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' - assert_response :success - assert_tag :tag => 'h2', :content => 'Projets' - assert_equal :fr, current_language - - # then an italien user - get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'it;q=0.8,en-us;q=0.5,en;q=0.3' - assert_response :success - assert_tag :tag => 'h2', :content => 'Progetti' - assert_equal :it, current_language - - # not a supported language: default language should be used - get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'zz' - assert_response :success - assert_tag :tag => 'h2', :content => 'Projects' - end - - def test_token_based_access_should_not_start_session - # issue of a private project - get 'issues/4.atom' - assert_response 302 - - rss_key = User.find(2).rss_key - get "issues/4.atom?key=#{rss_key}" - assert_response 200 - assert_nil session[:user_id] - end - - def test_missing_template_should_respond_with_404 - get '/login.png' - assert_response 404 - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0c62ded19e437ec59035548bf5b8cb434884827a.svn-base --- a/.svn/pristine/0c/0c62ded19e437ec59035548bf5b8cb434884827a.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1395 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Issue < ActiveRecord::Base - include Redmine::SafeAttributes - include Redmine::Utils::DateCalculation - - belongs_to :project - belongs_to :tracker - belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' - belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' - belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id' - belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' - belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' - belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' - - has_many :journals, :as => :journalized, :dependent => :destroy - has_many :visible_journals, - :class_name => 'Journal', - :as => :journalized, - :conditions => Proc.new { - ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false] - }, - :readonly => true - - has_many :time_entries, :dependent => :delete_all - has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" - - has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all - has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all - - acts_as_nested_set :scope => 'root_id', :dependent => :destroy - acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed - acts_as_customizable - acts_as_watchable - acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], - :include => [:project, :visible_journals], - # sort by id so that limited eager loading doesn't break with postgresql - :order_column => "#{table_name}.id" - acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, - :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, - :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } - - acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, - :author_key => :author_id - - DONE_RATIO_OPTIONS = %w(issue_field issue_status) - - attr_reader :current_journal - delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true - - validates_presence_of :subject, :priority, :project, :tracker, :author, :status - - validates_length_of :subject, :maximum => 255 - validates_inclusion_of :done_ratio, :in => 0..100 - validates_numericality_of :estimated_hours, :allow_nil => true - validate :validate_issue, :validate_required_fields - - scope :visible, - lambda {|*args| { :include => :project, - :conditions => Issue.visible_condition(args.shift || User.current, *args) } } - - scope :open, lambda {|*args| - is_closed = args.size > 0 ? !args.first : false - {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status} - } - - scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" - scope :on_active_project, :include => [:status, :project, :tracker], - :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] - - before_create :default_assign - before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change - after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} - after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal - # Should be after_create but would be called before previous after_save callbacks - after_save :after_create_from_copy - after_destroy :update_parent_attributes - - # Returns a SQL conditions string used to find all issues visible by the specified user - def self.visible_condition(user, options={}) - Project.allowed_to_condition(user, :view_issues, options) do |role, user| - if user.logged? - case role.issues_visibility - when 'all' - nil - when 'default' - user_ids = [user.id] + user.groups.map(&:id) - "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" - when 'own' - user_ids = [user.id] + user.groups.map(&:id) - "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" - else - '1=0' - end - else - "(#{table_name}.is_private = #{connection.quoted_false})" - end - end - end - - # Returns true if usr or current user is allowed to view the issue - def visible?(usr=nil) - (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| - if user.logged? - case role.issues_visibility - when 'all' - true - when 'default' - !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to)) - when 'own' - self.author == user || user.is_or_belongs_to?(assigned_to) - else - false - end - else - !self.is_private? - end - end - end - - def initialize(attributes=nil, *args) - super - if new_record? - # set default values for new records only - self.status ||= IssueStatus.default - self.priority ||= IssuePriority.default - self.watcher_user_ids = [] - end - end - - # AR#Persistence#destroy would raise and RecordNotFound exception - # if the issue was already deleted or updated (non matching lock_version). - # This is a problem when bulk deleting issues or deleting a project - # (because an issue may already be deleted if its parent was deleted - # first). - # The issue is reloaded by the nested_set before being deleted so - # the lock_version condition should not be an issue but we handle it. - def destroy - super - rescue ActiveRecord::RecordNotFound - # Stale or already deleted - begin - reload - rescue ActiveRecord::RecordNotFound - # The issue was actually already deleted - @destroyed = true - return freeze - end - # The issue was stale, retry to destroy - super - end - - def reload(*args) - @workflow_rule_by_attribute = nil - @assignable_versions = nil - super - end - - # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields - def available_custom_fields - (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] - end - - # Copies attributes from another issue, arg can be an id or an Issue - def copy_from(arg, options={}) - issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) - self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") - self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} - self.status = issue.status - self.author = User.current - unless options[:attachments] == false - self.attachments = issue.attachments.map do |attachement| - attachement.copy(:container => self) - end - end - @copied_from = issue - @copy_options = options - self - end - - # Returns an unsaved copy of the issue - def copy(attributes=nil, copy_options={}) - copy = self.class.new.copy_from(self, copy_options) - copy.attributes = attributes if attributes - copy - end - - # Returns true if the issue is a copy - def copy? - @copied_from.present? - end - - # Moves/copies an issue to a new project and tracker - # Returns the moved/copied issue on success, false on failure - def move_to_project(new_project, new_tracker=nil, options={}) - ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead." - - if options[:copy] - issue = self.copy - else - issue = self - end - - issue.init_journal(User.current, options[:notes]) - - # Preserve previous behaviour - # #move_to_project doesn't change tracker automatically - issue.send :project=, new_project, true - if new_tracker - issue.tracker = new_tracker - end - # Allow bulk setting of attributes on the issue - if options[:attributes] - issue.attributes = options[:attributes] - end - - issue.save ? issue : false - end - - def status_id=(sid) - self.status = nil - result = write_attribute(:status_id, sid) - @workflow_rule_by_attribute = nil - result - end - - def priority_id=(pid) - self.priority = nil - write_attribute(:priority_id, pid) - end - - def category_id=(cid) - self.category = nil - write_attribute(:category_id, cid) - end - - def fixed_version_id=(vid) - self.fixed_version = nil - write_attribute(:fixed_version_id, vid) - end - - def tracker_id=(tid) - self.tracker = nil - result = write_attribute(:tracker_id, tid) - @custom_field_values = nil - @workflow_rule_by_attribute = nil - result - end - - def project_id=(project_id) - if project_id.to_s != self.project_id.to_s - self.project = (project_id.present? ? Project.find_by_id(project_id) : nil) - end - end - - def project=(project, keep_tracker=false) - project_was = self.project - write_attribute(:project_id, project ? project.id : nil) - association_instance_set('project', project) - if project_was && project && project_was != project - @assignable_versions = nil - - unless keep_tracker || project.trackers.include?(tracker) - self.tracker = project.trackers.first - end - # Reassign to the category with same name if any - if category - self.category = project.issue_categories.find_by_name(category.name) - end - # Keep the fixed_version if it's still valid in the new_project - if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version) - self.fixed_version = nil - end - # Clear the parent task if it's no longer valid - unless valid_parent_project? - self.parent_issue_id = nil - end - @custom_field_values = nil - end - end - - def description=(arg) - if arg.is_a?(String) - arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") - end - write_attribute(:description, arg) - end - - # Overrides assign_attributes so that project and tracker get assigned first - def assign_attributes_with_project_and_tracker_first(new_attributes, *args) - return if new_attributes.nil? - attrs = new_attributes.dup - attrs.stringify_keys! - - %w(project project_id tracker tracker_id).each do |attr| - if attrs.has_key?(attr) - send "#{attr}=", attrs.delete(attr) - end - end - send :assign_attributes_without_project_and_tracker_first, attrs, *args - end - # Do not redefine alias chain on reload (see #4838) - alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first) - - def estimated_hours=(h) - write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) - end - - safe_attributes 'project_id', - :if => lambda {|issue, user| - if issue.new_record? - issue.copy? - elsif user.allowed_to?(:move_issues, issue.project) - projects = Issue.allowed_target_projects_on_move(user) - projects.include?(issue.project) && projects.size > 1 - end - } - - safe_attributes 'tracker_id', - 'status_id', - 'category_id', - 'assigned_to_id', - 'priority_id', - 'fixed_version_id', - 'subject', - 'description', - 'start_date', - 'due_date', - 'done_ratio', - 'estimated_hours', - 'custom_field_values', - 'custom_fields', - 'lock_version', - 'notes', - :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } - - safe_attributes 'status_id', - 'assigned_to_id', - 'fixed_version_id', - 'done_ratio', - 'lock_version', - 'notes', - :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } - - safe_attributes 'notes', - :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)} - - safe_attributes 'private_notes', - :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} - - safe_attributes 'watcher_user_ids', - :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} - - safe_attributes 'is_private', - :if => lambda {|issue, user| - user.allowed_to?(:set_issues_private, issue.project) || - (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project)) - } - - safe_attributes 'parent_issue_id', - :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && - user.allowed_to?(:manage_subtasks, issue.project)} - - def safe_attribute_names(user=nil) - names = super - names -= disabled_core_fields - names -= read_only_attribute_names(user) - names - end - - # Safely sets attributes - # Should be called from controllers instead of #attributes= - # attr_accessible is too rough because we still want things like - # Issue.new(:project => foo) to work - def safe_attributes=(attrs, user=User.current) - return unless attrs.is_a?(Hash) - - attrs = attrs.dup - - # Project and Tracker must be set before since new_statuses_allowed_to depends on it. - if (p = attrs.delete('project_id')) && safe_attribute?('project_id') - if allowed_target_projects(user).collect(&:id).include?(p.to_i) - self.project_id = p - end - end - - if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id') - self.tracker_id = t - end - - if (s = attrs.delete('status_id')) && safe_attribute?('status_id') - if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i) - self.status_id = s - end - end - - attrs = delete_unsafe_attributes(attrs, user) - return if attrs.empty? - - unless leaf? - attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} - end - - if attrs['parent_issue_id'].present? - s = attrs['parent_issue_id'].to_s - unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) - @invalid_parent_issue_id = attrs.delete('parent_issue_id') - end - end - - if attrs['custom_field_values'].present? - attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s} - end - - if attrs['custom_fields'].present? - attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s} - end - - # mass-assignment security bypass - assign_attributes attrs, :without_protection => true - end - - def disabled_core_fields - tracker ? tracker.disabled_core_fields : [] - end - - # Returns the custom_field_values that can be edited by the given user - def editable_custom_field_values(user=nil) - custom_field_values.reject do |value| - read_only_attribute_names(user).include?(value.custom_field_id.to_s) - end - end - - # Returns the names of attributes that are read-only for user or the current user - # For users with multiple roles, the read-only fields are the intersection of - # read-only fields of each role - # The result is an array of strings where sustom fields are represented with their ids - # - # Examples: - # issue.read_only_attribute_names # => ['due_date', '2'] - # issue.read_only_attribute_names(user) # => [] - def read_only_attribute_names(user=nil) - workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys - end - - # Returns the names of required attributes for user or the current user - # For users with multiple roles, the required fields are the intersection of - # required fields of each role - # The result is an array of strings where sustom fields are represented with their ids - # - # Examples: - # issue.required_attribute_names # => ['due_date', '2'] - # issue.required_attribute_names(user) # => [] - def required_attribute_names(user=nil) - workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys - end - - # Returns true if the attribute is required for user - def required_attribute?(name, user=nil) - required_attribute_names(user).include?(name.to_s) - end - - # Returns a hash of the workflow rule by attribute for the given user - # - # Examples: - # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} - def workflow_rule_by_attribute(user=nil) - return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? - - user_real = user || User.current - roles = user_real.admin ? Role.all : user_real.roles_for_project(project) - return {} if roles.empty? - - result = {} - workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all - if workflow_permissions.any? - workflow_rules = workflow_permissions.inject({}) do |h, wp| - h[wp.field_name] ||= [] - h[wp.field_name] << wp.rule - h - end - workflow_rules.each do |attr, rules| - next if rules.size < roles.size - uniq_rules = rules.uniq - if uniq_rules.size == 1 - result[attr] = uniq_rules.first - else - result[attr] = 'required' - end - end - end - @workflow_rule_by_attribute = result if user.nil? - result - end - private :workflow_rule_by_attribute - - def done_ratio - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio - status.default_done_ratio - else - read_attribute(:done_ratio) - end - end - - def self.use_status_for_done_ratio? - Setting.issue_done_ratio == 'issue_status' - end - - def self.use_field_for_done_ratio? - Setting.issue_done_ratio == 'issue_field' - end - - def validate_issue - if due_date.nil? && @attributes['due_date'].present? - errors.add :due_date, :not_a_date - end - - if start_date.nil? && @attributes['start_date'].present? - errors.add :start_date, :not_a_date - end - - if due_date && start_date && due_date < start_date - errors.add :due_date, :greater_than_start_date - end - - if start_date && soonest_start && start_date < soonest_start - errors.add :start_date, :invalid - end - - if fixed_version - if !assignable_versions.include?(fixed_version) - errors.add :fixed_version_id, :inclusion - elsif reopened? && fixed_version.closed? - errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version) - end - end - - # Checks that the issue can not be added/moved to a disabled tracker - if project && (tracker_id_changed? || project_id_changed?) - unless project.trackers.include?(tracker) - errors.add :tracker_id, :inclusion - end - end - - # Checks parent issue assignment - if @invalid_parent_issue_id.present? - errors.add :parent_issue_id, :invalid - elsif @parent_issue - if !valid_parent_project?(@parent_issue) - errors.add :parent_issue_id, :invalid - elsif !new_record? - # moving an existing issue - if @parent_issue.root_id != root_id - # we can always move to another tree - elsif move_possible?(@parent_issue) - # move accepted inside tree - else - errors.add :parent_issue_id, :invalid - end - end - end - end - - # Validates the issue against additional workflow requirements - def validate_required_fields - user = new_record? ? author : current_journal.try(:user) - - required_attribute_names(user).each do |attribute| - if attribute =~ /^\d+$/ - attribute = attribute.to_i - v = custom_field_values.detect {|v| v.custom_field_id == attribute } - if v && v.value.blank? - errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') - end - else - if respond_to?(attribute) && send(attribute).blank? - errors.add attribute, :blank - end - end - end - end - - # Set the done_ratio using the status if that setting is set. This will keep the done_ratios - # even if the user turns off the setting later - def update_done_ratio_from_issue_status - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio - self.done_ratio = status.default_done_ratio - end - end - - def init_journal(user, notes = "") - @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) - if new_record? - @current_journal.notify = false - else - @attributes_before_change = attributes.dup - @custom_values_before_change = {} - self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } - end - @current_journal - end - - # Returns the id of the last journal or nil - def last_journal_id - if new_record? - nil - else - journals.maximum(:id) - end - end - - # Returns a scope for journals that have an id greater than journal_id - def journals_after(journal_id) - scope = journals.reorder("#{Journal.table_name}.id ASC") - if journal_id.present? - scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i) - end - scope - end - - # Return true if the issue is closed, otherwise false - def closed? - self.status.is_closed? - end - - # Return true if the issue is being reopened - def reopened? - if !new_record? && status_id_changed? - status_was = IssueStatus.find_by_id(status_id_was) - status_new = IssueStatus.find_by_id(status_id) - if status_was && status_new && status_was.is_closed? && !status_new.is_closed? - return true - end - end - false - end - - # Return true if the issue is being closed - def closing? - if !new_record? && status_id_changed? - status_was = IssueStatus.find_by_id(status_id_was) - status_new = IssueStatus.find_by_id(status_id) - if status_was && status_new && !status_was.is_closed? && status_new.is_closed? - return true - end - end - false - end - - # Returns true if the issue is overdue - def overdue? - !due_date.nil? && (due_date < Date.today) && !status.is_closed? - end - - # Is the amount of work done less than it should for the due date - def behind_schedule? - return false if start_date.nil? || due_date.nil? - done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor - return done_date <= Date.today - end - - # Does this issue have children? - def children? - !leaf? - end - - # Users the issue can be assigned to - def assignable_users - users = project.assignable_users - users << author if author - users << assigned_to if assigned_to - users.uniq.sort - end - - # Versions that the issue can be assigned to - def assignable_versions - return @assignable_versions if @assignable_versions - - versions = project.shared_versions.open.all - if fixed_version - if fixed_version_id_changed? - # nothing to do - elsif project_id_changed? - if project.shared_versions.include?(fixed_version) - versions << fixed_version - end - else - versions << fixed_version - end - end - @assignable_versions = versions.uniq.sort - end - - # Returns true if this issue is blocked by another issue that is still open - def blocked? - !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? - end - - # Returns an array of statuses that user is able to apply - def new_statuses_allowed_to(user=User.current, include_default=false) - if new_record? && @copied_from - [IssueStatus.default, @copied_from.status].compact.uniq.sort - else - initial_status = nil - if new_record? - initial_status = IssueStatus.default - elsif status_id_was - initial_status = IssueStatus.find_by_id(status_id_was) - end - initial_status ||= status - - statuses = initial_status.find_new_statuses_allowed_to( - user.admin ? Role.all : user.roles_for_project(project), - tracker, - author == user, - assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id - ) - statuses << initial_status unless statuses.empty? - statuses << IssueStatus.default if include_default - statuses = statuses.compact.uniq.sort - blocked? ? statuses.reject {|s| s.is_closed?} : statuses - end - end - - def assigned_to_was - if assigned_to_id_changed? && assigned_to_id_was.present? - @assigned_to_was ||= User.find_by_id(assigned_to_id_was) - end - end - - # Returns the users that should be notified - def notified_users - notified = [] - # Author and assignee are always notified unless they have been - # locked or don't want to be notified - notified << author if author - if assigned_to - notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to]) - end - if assigned_to_was - notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was]) - end - notified = notified.select {|u| u.active? && u.notify_about?(self)} - - notified += project.notified_users - notified.uniq! - # Remove users that can not view the issue - notified.reject! {|user| !visible?(user)} - notified - end - - # Returns the email addresses that should be notified - def recipients - notified_users.collect(&:mail) - end - - # Returns the number of hours spent on this issue - def spent_hours - @spent_hours ||= time_entries.sum(:hours) || 0 - end - - # Returns the total number of hours spent on this issue and its descendants - # - # Example: - # spent_hours => 0.0 - # spent_hours => 50.2 - def total_spent_hours - @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", - :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0 - end - - def relations - @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort) - end - - # Preloads relations for a collection of issues - def self.load_relations(issues) - if issues.any? - relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}]) - issues.each do |issue| - issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id} - end - end - end - - # Preloads visible spent time for a collection of issues - def self.load_visible_spent_hours(issues, user=User.current) - if issues.any? - hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id) - issues.each do |issue| - issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0) - end - end - end - - # Preloads visible relations for a collection of issues - def self.load_visible_relations(issues, user=User.current) - if issues.any? - issue_ids = issues.map(&:id) - # Relations with issue_from in given issues and visible issue_to - relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all - # Relations with issue_to in given issues and visible issue_from - relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all - - issues.each do |issue| - relations = - relations_from.select {|relation| relation.issue_from_id == issue.id} + - relations_to.select {|relation| relation.issue_to_id == issue.id} - - issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort) - end - end - end - - # Finds an issue relation given its id. - def find_relation(relation_id) - IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) - end - - def all_dependent_issues(except=[]) - except << self - dependencies = [] - relations_from.each do |relation| - if relation.issue_to && !except.include?(relation.issue_to) - dependencies << relation.issue_to - dependencies += relation.issue_to.all_dependent_issues(except) - end - end - dependencies - end - - # Returns an array of issues that duplicate this one - def duplicates - relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} - end - - # Returns the due date or the target due date if any - # Used on gantt chart - def due_before - due_date || (fixed_version ? fixed_version.effective_date : nil) - end - - # Returns the time scheduled for this issue. - # - # Example: - # Start Date: 2/26/09, End Date: 3/04/09 - # duration => 6 - def duration - (start_date && due_date) ? due_date - start_date : 0 - end - - # Returns the duration in working days - def working_duration - (start_date && due_date) ? working_days(start_date, due_date) : 0 - end - - def soonest_start(reload=false) - @soonest_start = nil if reload - @soonest_start ||= ( - relations_to(reload).collect{|relation| relation.successor_soonest_start} + - ancestors.collect(&:soonest_start) - ).compact.max - end - - # Sets start_date on the given date or the next working day - # and changes due_date to keep the same working duration. - def reschedule_on(date) - wd = working_duration - date = next_working_date(date) - self.start_date = date - self.due_date = add_working_days(date, wd) - end - - # Reschedules the issue on the given date or the next working day and saves the record. - # If the issue is a parent task, this is done by rescheduling its subtasks. - def reschedule_on!(date) - return if date.nil? - if leaf? - if start_date.nil? || start_date != date - if start_date && start_date > date - # Issue can not be moved earlier than its soonest start date - date = [soonest_start(true), date].compact.max - end - reschedule_on(date) - begin - save - rescue ActiveRecord::StaleObjectError - reload - reschedule_on(date) - save - end - end - else - leaves.each do |leaf| - if leaf.start_date - # Only move subtask if it starts at the same date as the parent - # or if it starts before the given date - if start_date == leaf.start_date || date > leaf.start_date - leaf.reschedule_on!(date) - end - else - leaf.reschedule_on!(date) - end - end - end - end - - def <=>(issue) - if issue.nil? - -1 - elsif root_id != issue.root_id - (root_id || 0) <=> (issue.root_id || 0) - else - (lft || 0) <=> (issue.lft || 0) - end - end - - def to_s - "#{tracker} ##{id}: #{subject}" - end - - # Returns a string of css classes that apply to the issue - def css_classes - s = "issue status-#{status_id} #{priority.try(:css_classes)}" - s << ' closed' if closed? - s << ' overdue' if overdue? - s << ' child' if child? - s << ' parent' unless leaf? - s << ' private' if is_private? - s << ' created-by-me' if User.current.logged? && author_id == User.current.id - s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id - s - end - - # Saves an issue and a time_entry from the parameters - def save_issue_with_child_records(params, existing_time_entry=nil) - Issue.transaction do - if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) - @time_entry = existing_time_entry || TimeEntry.new - @time_entry.project = project - @time_entry.issue = self - @time_entry.user = User.current - @time_entry.spent_on = User.current.today - @time_entry.attributes = params[:time_entry] - self.time_entries << @time_entry - end - - # TODO: Rename hook - Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) - if save - # TODO: Rename hook - Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) - else - raise ActiveRecord::Rollback - end - end - end - - # Unassigns issues from +version+ if it's no longer shared with issue's project - def self.update_versions_from_sharing_change(version) - # Update issues assigned to the version - update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) - end - - # Unassigns issues from versions that are no longer shared - # after +project+ was moved - def self.update_versions_from_hierarchy_change(project) - moved_project_ids = project.self_and_descendants.reload.collect(&:id) - # Update issues of the moved projects and issues assigned to a version of a moved project - Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) - end - - def parent_issue_id=(arg) - s = arg.to_s.strip.presence - if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1])) - @parent_issue.id - else - @parent_issue = nil - @invalid_parent_issue_id = arg - end - end - - def parent_issue_id - if @invalid_parent_issue_id - @invalid_parent_issue_id - elsif instance_variable_defined? :@parent_issue - @parent_issue.nil? ? nil : @parent_issue.id - else - parent_id - end - end - - # Returns true if issue's project is a valid - # parent issue project - def valid_parent_project?(issue=parent) - return true if issue.nil? || issue.project_id == project_id - - case Setting.cross_project_subtasks - when 'system' - true - when 'tree' - issue.project.root == project.root - when 'hierarchy' - issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project) - when 'descendants' - issue.project.is_or_is_ancestor_of?(project) - else - false - end - end - - # Extracted from the ReportsController. - def self.by_tracker(project) - count_and_group_by(:project => project, - :field => 'tracker_id', - :joins => Tracker.table_name) - end - - def self.by_version(project) - count_and_group_by(:project => project, - :field => 'fixed_version_id', - :joins => Version.table_name) - end - - def self.by_priority(project) - count_and_group_by(:project => project, - :field => 'priority_id', - :joins => IssuePriority.table_name) - end - - def self.by_category(project) - count_and_group_by(:project => project, - :field => 'category_id', - :joins => IssueCategory.table_name) - end - - def self.by_assigned_to(project) - count_and_group_by(:project => project, - :field => 'assigned_to_id', - :joins => User.table_name) - end - - def self.by_author(project) - count_and_group_by(:project => project, - :field => 'author_id', - :joins => User.table_name) - end - - def self.by_subproject(project) - ActiveRecord::Base.connection.select_all("select s.id as status_id, - s.is_closed as closed, - #{Issue.table_name}.project_id as project_id, - count(#{Issue.table_name}.id) as total - from - #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s - where - #{Issue.table_name}.status_id=s.id - and #{Issue.table_name}.project_id = #{Project.table_name}.id - and #{visible_condition(User.current, :project => project, :with_subprojects => true)} - and #{Issue.table_name}.project_id <> #{project.id} - group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any? - end - # End ReportsController extraction - - # Returns an array of projects that user can assign the issue to - def allowed_target_projects(user=User.current) - if new_record? - Project.all(:conditions => Project.allowed_to_condition(user, :add_issues)) - else - self.class.allowed_target_projects_on_move(user) - end - end - - # Returns an array of projects that user can move issues to - def self.allowed_target_projects_on_move(user=User.current) - Project.all(:conditions => Project.allowed_to_condition(user, :move_issues)) - end - - private - - def after_project_change - # Update project_id on related time entries - TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id}) - - # Delete issue relations - unless Setting.cross_project_issue_relations? - relations_from.clear - relations_to.clear - end - - # Move subtasks that were in the same project - children.each do |child| - next unless child.project_id == project_id_was - # Change project and keep project - child.send :project=, project, true - unless child.save - raise ActiveRecord::Rollback - end - end - end - - # Callback for after the creation of an issue by copy - # * adds a "copied to" relation with the copied issue - # * copies subtasks from the copied issue - def after_create_from_copy - return unless copy? && !@after_create_from_copy_handled - - if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false - relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO) - unless relation.save - logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger - end - end - - unless @copied_from.leaf? || @copy_options[:subtasks] == false - @copied_from.children.each do |child| - unless child.visible? - # Do not copy subtasks that are not visible to avoid potential disclosure of private data - logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger - next - end - copy = Issue.new.copy_from(child, @copy_options) - copy.author = author - copy.project = project - copy.parent_issue_id = id - # Children subtasks are copied recursively - unless copy.save - logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger - end - end - end - @after_create_from_copy_handled = true - end - - def update_nested_set_attributes - if root_id.nil? - # issue was just created - self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) - set_default_left_and_right - Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id]) - if @parent_issue - move_to_child_of(@parent_issue) - end - reload - elsif parent_issue_id != parent_id - former_parent_id = parent_id - # moving an existing issue - if @parent_issue && @parent_issue.root_id == root_id - # inside the same tree - move_to_child_of(@parent_issue) - else - # to another tree - unless root? - move_to_right_of(root) - reload - end - old_root_id = root_id - self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id ) - target_maxright = nested_set_scope.maximum(right_column_name) || 0 - offset = target_maxright + 1 - lft - Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}", - ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]) - self[left_column_name] = lft + offset - self[right_column_name] = rgt + offset - if @parent_issue - move_to_child_of(@parent_issue) - end - end - reload - # delete invalid relations of all descendants - self_and_descendants.each do |issue| - issue.relations.each do |relation| - relation.destroy unless relation.valid? - end - end - # update former parent - recalculate_attributes_for(former_parent_id) if former_parent_id - end - remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) - end - - def update_parent_attributes - recalculate_attributes_for(parent_id) if parent_id - end - - def recalculate_attributes_for(issue_id) - if issue_id && p = Issue.find_by_id(issue_id) - # priority = highest priority of children - if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority) - p.priority = IssuePriority.find_by_position(priority_position) - end - - # start/due dates = lowest/highest dates of children - p.start_date = p.children.minimum(:start_date) - p.due_date = p.children.maximum(:due_date) - if p.start_date && p.due_date && p.due_date < p.start_date - p.start_date, p.due_date = p.due_date, p.start_date - end - - # done ratio = weighted average ratio of leaves - unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio - leaves_count = p.leaves.count - if leaves_count > 0 - average = p.leaves.average(:estimated_hours).to_f - if average == 0 - average = 1 - end - done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f - progress = done / (average * leaves_count) - p.done_ratio = progress.round - end - end - - # estimate = sum of leaves estimates - p.estimated_hours = p.leaves.sum(:estimated_hours).to_f - p.estimated_hours = nil if p.estimated_hours == 0.0 - - # ancestors will be recursively updated - p.save(:validate => false) - end - end - - # Update issues so their versions are not pointing to a - # fixed_version that is not shared with the issue's project - def self.update_versions(conditions=nil) - # Only need to update issues with a fixed_version from - # a different project and that is not systemwide shared - Issue.scoped(:conditions => conditions).all( - :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" + - " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + - " AND #{Version.table_name}.sharing <> 'system'", - :include => [:project, :fixed_version] - ).each do |issue| - next if issue.project.nil? || issue.fixed_version.nil? - unless issue.project.shared_versions.include?(issue.fixed_version) - issue.init_journal(User.current) - issue.fixed_version = nil - issue.save - end - end - end - - # Callback on file attachment - def attachment_added(obj) - if @current_journal && !obj.new_record? - @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename) - end - end - - # Callback on attachment deletion - def attachment_removed(obj) - if @current_journal && !obj.new_record? - @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename) - @current_journal.save - end - end - - # Default assignment based on category - def default_assign - if assigned_to.nil? && category && category.assigned_to - self.assigned_to = category.assigned_to - end - end - - # Updates start/due dates of following issues - def reschedule_following_issues - if start_date_changed? || due_date_changed? - relations_from.each do |relation| - relation.set_issue_to_dates - end - end - end - - # Closes duplicates if the issue is being closed - def close_duplicates - if closing? - duplicates.each do |duplicate| - # Reload is need in case the duplicate was updated by a previous duplicate - duplicate.reload - # Don't re-close it if it's already closed - next if duplicate.closed? - # Same user and notes - if @current_journal - duplicate.init_journal(@current_journal.user, @current_journal.notes) - end - duplicate.update_attribute :status, self.status - end - end - end - - # Make sure updated_on is updated when adding a note - def force_updated_on_change - if @current_journal - self.updated_on = current_time_from_proper_timezone - end - end - - # Saves the changes in a Journal - # Called after_save - def create_journal - if @current_journal - # attributes changes - if @attributes_before_change - (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| - before = @attributes_before_change[c] - after = send(c) - next if before == after || (before.blank? && after.blank?) - @current_journal.details << JournalDetail.new(:property => 'attr', - :prop_key => c, - :old_value => before, - :value => after) - } - end - if @custom_values_before_change - # custom fields changes - custom_field_values.each {|c| - before = @custom_values_before_change[c.custom_field_id] - after = c.value - next if before == after || (before.blank? && after.blank?) - - if before.is_a?(Array) || after.is_a?(Array) - before = [before] unless before.is_a?(Array) - after = [after] unless after.is_a?(Array) - - # values removed - (before - after).reject(&:blank?).each do |value| - @current_journal.details << JournalDetail.new(:property => 'cf', - :prop_key => c.custom_field_id, - :old_value => value, - :value => nil) - end - # values added - (after - before).reject(&:blank?).each do |value| - @current_journal.details << JournalDetail.new(:property => 'cf', - :prop_key => c.custom_field_id, - :old_value => nil, - :value => value) - end - else - @current_journal.details << JournalDetail.new(:property => 'cf', - :prop_key => c.custom_field_id, - :old_value => before, - :value => after) - end - } - end - @current_journal.save - # reset current journal - init_journal @current_journal.user, @current_journal.notes - end - end - - # Query generator for selecting groups of issue counts for a project - # based on specific criteria - # - # Options - # * project - Project to search in. - # * field - String. Issue field to key off of in the grouping. - # * joins - String. The table name to join against. - def self.count_and_group_by(options) - project = options.delete(:project) - select_field = options.delete(:field) - joins = options.delete(:joins) - - where = "#{Issue.table_name}.#{select_field}=j.id" - - ActiveRecord::Base.connection.select_all("select s.id as status_id, - s.is_closed as closed, - j.id as #{select_field}, - count(#{Issue.table_name}.id) as total - from - #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j - where - #{Issue.table_name}.status_id=s.id - and #{where} - and #{Issue.table_name}.project_id=#{Project.table_name}.id - and #{visible_condition(User.current, :project => project)} - group by s.id, s.is_closed, j.id") - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0c9dc58fe829b59060fa2ba2bfeb1db760e787c2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c9dc58fe829b59060fa2ba2bfeb1db760e787c2.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,307 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RepositoryBazaarTest < ActiveSupport::TestCase + fixtures :projects + + include Redmine::I18n + + REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s + REPOSITORY_PATH_TRUNK = File.join(REPOSITORY_PATH, "trunk") + NUM_REV = 4 + + REPOSITORY_PATH_NON_ASCII = Rails.root.join(REPOSITORY_PATH + '/' + 'non_ascii').to_s + + # Bazaar core does not support xml output such as Subversion and Mercurial. + # "bzr" command output and command line parameter depend on locale. + # So, non ASCII path tests cannot run independent locale. + # + # If you want to run Bazaar non ASCII path tests on Linux *Ruby 1.9*, + # you need to set locale character set "ISO-8859-1". + # E.g. "LANG=en_US.ISO-8859-1". + # On Linux other platforms (e.g. Ruby 1.8, JRuby), + # you need to set "RUN_LATIN1_OUTPUT_TEST = true" manually. + # + # On Windows, because it is too hard to change system locale, + # you cannot run Bazaar non ASCII path tests. + # + RUN_LATIN1_OUTPUT_TEST = (RUBY_PLATFORM != 'java' && + REPOSITORY_PATH.respond_to?(:force_encoding) && + Encoding.locale_charmap == "ISO-8859-1") + + CHAR_1_UTF8_HEX = "\xc3\x9c" + CHAR_1_LATIN1_HEX = "\xdc" + + def setup + @project = Project.find(3) + @repository = Repository::Bazaar.create( + :project => @project, :url => REPOSITORY_PATH_TRUNK, + :log_encoding => 'UTF-8') + assert @repository + @char_1_utf8 = CHAR_1_UTF8_HEX.dup + @char_1_ascii8bit = CHAR_1_LATIN1_HEX.dup + if @char_1_utf8.respond_to?(:force_encoding) + @char_1_utf8.force_encoding('UTF-8') + @char_1_ascii8bit.force_encoding('ASCII-8BIT') + end + end + + def test_blank_path_to_repository_error_message + set_language_if_valid 'en' + repo = Repository::Bazaar.new( + :project => @project, + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include "Path to repository can't be blank", + repo.errors.full_messages + end + + def test_blank_path_to_repository_error_message_fr + set_language_if_valid 'fr' + str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + repo = Repository::Bazaar.new( + :project => @project, + :url => "", + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include str, repo.errors.full_messages + end + + if File.directory?(REPOSITORY_PATH_TRUNK) + def test_fetch_changesets_from_scratch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + + assert_equal NUM_REV, @repository.changesets.count + assert_equal 9, @repository.filechanges.count + assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments + end + + def test_fetch_changesets_incremental + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + # Remove changesets with revision > 5 + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 2} + @project.reload + @repository.reload + assert_equal 2, @repository.changesets.count + + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + end + + def test_entries + entries = @repository.entries + assert_kind_of Redmine::Scm::Adapters::Entries, entries + assert_equal 2, entries.size + + assert_equal 'dir', entries[0].kind + assert_equal 'directory', entries[0].name + assert_equal 'directory', entries[0].path + + assert_equal 'file', entries[1].kind + assert_equal 'doc-mkdir.txt', entries[1].name + assert_equal 'doc-mkdir.txt', entries[1].path + end + + def test_entries_in_subdirectory + entries = @repository.entries('directory') + assert_equal 3, entries.size + + assert_equal 'file', entries.last.kind + assert_equal 'edit.png', entries.last.name + assert_equal 'directory/edit.png', entries.last.path + end + + def test_previous + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('3') + assert_equal @repository.find_changeset_by_name('2'), changeset.previous + end + + def test_previous_nil + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('1') + assert_nil changeset.previous + end + + def test_next + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('2') + assert_equal @repository.find_changeset_by_name('3'), changeset.next + end + + def test_next_nil + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('4') + assert_nil changeset.next + end + + if File.directory?(REPOSITORY_PATH_NON_ASCII) && RUN_LATIN1_OUTPUT_TEST + def test_cat_latin1_path + latin1_repo = create_latin1_repo + buf = latin1_repo.cat( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2) + assert buf + lines = buf.split("\n") + assert_equal 2, lines.length + assert_equal 'It is written in Python.', lines[1] + + buf = latin1_repo.cat( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2) + assert buf + lines = buf.split("\n") + assert_equal 1, lines.length + assert_equal "test-#{@char_1_ascii8bit}.txt", lines[0] + end + + def test_annotate_latin1_path + latin1_repo = create_latin1_repo + ann1 = latin1_repo.annotate( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2) + assert_equal 2, ann1.lines.size + assert_equal '2', ann1.revisions[0].identifier + assert_equal 'test00@', ann1.revisions[0].author + assert_equal 'It is written in Python.', ann1.lines[1] + ann2 = latin1_repo.annotate( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2) + assert_equal 1, ann2.lines.size + assert_equal '2', ann2.revisions[0].identifier + assert_equal 'test00@', ann2.revisions[0].author + assert_equal "test-#{@char_1_ascii8bit}.txt", ann2.lines[0] + end + + def test_diff_latin1_path + latin1_repo = create_latin1_repo + diff1 = latin1_repo.diff( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2, 1) + assert_equal 7, diff1.size + buf = diff1[5].gsub(/\r\n|\r|\n/, "") + assert_equal "+test-#{@char_1_ascii8bit}.txt", buf + end + + def test_entries_latin1_path + latin1_repo = create_latin1_repo + entries = latin1_repo.entries("test-#{@char_1_utf8}-dir", 2) + assert_kind_of Redmine::Scm::Adapters::Entries, entries + assert_equal 3, entries.size + assert_equal 'file', entries[1].kind + assert_equal "test-#{@char_1_utf8}-1.txt", entries[0].name + assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", entries[0].path + end + + def test_entry_latin1_path + latin1_repo = create_latin1_repo + ["test-#{@char_1_utf8}-dir", + "/test-#{@char_1_utf8}-dir", + "/test-#{@char_1_utf8}-dir/" + ].each do |path| + entry = latin1_repo.entry(path, 2) + assert_equal "test-#{@char_1_utf8}-dir", entry.path + assert_equal "dir", entry.kind + end + ["test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", + "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt" + ].each do |path| + entry = latin1_repo.entry(path, 2) + assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", + entry.path + assert_equal "file", entry.kind + end + end + + def test_changeset_latin1_path + latin1_repo = create_latin1_repo + assert_equal 0, latin1_repo.changesets.count + latin1_repo.fetch_changesets + @project.reload + assert_equal 3, latin1_repo.changesets.count + + cs2 = latin1_repo.changesets.find_by_revision('2') + assert_not_nil cs2 + assert_equal "test-#{@char_1_utf8}", cs2.comments + c2 = cs2.filechanges.sort_by(&:path) + assert_equal 4, c2.size + assert_equal 'A', c2[0].action + assert_equal "/test-#{@char_1_utf8}-dir/", c2[0].path + assert_equal 'A', c2[1].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", c2[1].path + assert_equal 'A', c2[2].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c2[2].path + assert_equal 'A', c2[3].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c2[3].path + + cs3 = latin1_repo.changesets.find_by_revision('3') + assert_not_nil cs3 + assert_equal "modify, move and delete #{@char_1_utf8} files", cs3.comments + c3 = cs3.filechanges.sort_by(&:path) + assert_equal 3, c3.size + assert_equal 'M', c3[0].action + assert_equal "/test-#{@char_1_utf8}-1.txt", c3[0].path + assert_equal 'D', c3[1].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c3[1].path + assert_equal 'M', c3[2].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c3[2].path + end + else + msg = "Bazaar non ASCII output test cannot run this environment." + "\n" + if msg.respond_to?(:force_encoding) + msg += "Encoding.locale_charmap: " + Encoding.locale_charmap + "\n" + end + puts msg + end + + private + + def create_latin1_repo + repo = Repository::Bazaar.create( + :project => @project, + :identifier => 'latin1', + :url => REPOSITORY_PATH_NON_ASCII, + :log_encoding => 'ISO-8859-1' + ) + assert repo + repo + end + else + puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0cb1b271165f09325935c33b90607362679e5b26.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0cb1b271165f09325935c33b90607362679e5b26.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,23 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 UserCustomField < CustomField + def type_name + :label_user_plural + end +end + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0cb33e746ce4443d0cb3bfc0a14c8cc1559914ac.svn-base --- a/.svn/pristine/0c/0cb33e746ce4443d0cb3bfc0a14c8cc1559914ac.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -<%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %> - ----------------------------------------- -<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :issue_url => @issue_url } %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0cc35d9ebd9809126c44031aad1c4acc705d8aec.svn-base --- a/.svn/pristine/0c/0cc35d9ebd9809126c44031aad1c4acc705d8aec.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 IssueObserver < ActiveRecord::Observer - def after_create(issue) - Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added') - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0c/0ccef3aea9c789ba7ff6975c6015bb677b5a97a9.svn-base --- a/.svn/pristine/0c/0ccef3aea9c789ba7ff6975c6015bb677b5a97a9.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 - class Diff - include ERB::Util - include ActionView::Helpers::TagHelper - include ActionView::Helpers::TextHelper - attr_reader :diff, :words - - def initialize(content_to, content_from) - @words = content_to.to_s.split(/(\s+)/) - @words = @words.select {|word| word != ' '} - words_from = content_from.to_s.split(/(\s+)/) - words_from = words_from.select {|word| word != ' '} - @diff = words_from.diff @words - end - - def to_html - words = self.words.collect{|word| h(word)} - words_add = 0 - words_del = 0 - dels = 0 - del_off = 0 - diff.diffs.each do |diff| - add_at = nil - add_to = nil - del_at = nil - deleted = "" - diff.each do |change| - pos = change[1] - if change[0] == "+" - add_at = pos + dels unless add_at - add_to = pos + dels - words_add += 1 - else - del_at = pos unless del_at - deleted << ' ' unless deleted.empty? - deleted << h(change[2]) - words_del += 1 - end - end - if add_at - words[add_at] = ''.html_safe + words[add_at] - words[add_to] = words[add_to] + ''.html_safe - end - if del_at - words.insert del_at - del_off + dels + words_add, ''.html_safe + deleted + ''.html_safe - dels += 1 - del_off += words_del - words_del = 0 - end - end - words.join(' ').html_safe - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0d0d229a445c7f425386e30fa2d56de79c1a0ed1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d0d229a445c7f425386e30fa2d56de79c1a0ed1.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,48 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 GanttsController < ApplicationController + menu_item :gantt + before_filter :find_optional_project + + rescue_from Query::StatementInvalid, :with => :query_statement_invalid + + helper :gantt + helper :issues + helper :projects + helper :queries + include QueriesHelper + helper :sort + include SortHelper + include Redmine::Export::PDF + + def show + @gantt = Redmine::Helpers::Gantt.new(params) + @gantt.project = @project + retrieve_query + @query.group_by = nil + @gantt.query = @query if @query.valid? + + basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' + + respond_to do |format| + format.html { render :action => "show", :layout => !request.xhr? } + format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') + format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") } + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0d10980667aaa496ecffdcff030d6ecd20b65b5f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d10980667aaa496ecffdcff030d6ecd20b65b5f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,145 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueRelationsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :issue_relations, + :enabled_modules, + :enumerations, + :trackers, + :projects_trackers + + def setup + User.current = nil + @request.session[:user_id] = 3 + end + + def test_create + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.order('id DESC').first + assert_equal 1, relation.issue_from_id + assert_equal 2, relation.issue_to_id + assert_equal 'relates', relation.relation_type + end + + def test_create_xhr + assert_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => 3, :relation => {:issue_to_id => '1', :relation_type => 'relates', :delay => ''} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + relation = IssueRelation.order('id DESC').first + assert_equal 3, relation.issue_from_id + assert_equal 1, relation.issue_to_id + + assert_match /Bug #1/, response.body + end + + def test_create_should_accept_id_with_hash + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '#2', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.order('id DESC').first + assert_equal 2, relation.issue_to_id + end + + def test_create_should_strip_id + assert_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => ' 2 ', :relation_type => 'relates', :delay => ''} + end + relation = IssueRelation.order('id DESC').first + assert_equal 2, relation.issue_to_id + end + + def test_create_should_not_break_with_non_numerical_id + assert_no_difference 'IssueRelation.count' do + assert_nothing_raised do + post :create, :issue_id => 1, + :relation => {:issue_to_id => 'foo', :relation_type => 'relates', :delay => ''} + end + end + end + + def test_create_follows_relation_should_update_relations_list + issue1 = Issue.generate!(:subject => 'Followed issue', :start_date => Date.yesterday, :due_date => Date.today) + issue2 = Issue.generate! + + assert_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => issue2.id, + :relation => {:issue_to_id => issue1.id, :relation_type => 'follows', :delay => ''} + end + assert_match /Followed issue/, response.body + end + + def test_should_create_relations_with_visible_issues_only + Setting.cross_project_issue_relations = '1' + assert_nil Issue.visible(User.find(3)).find_by_id(4) + + assert_no_difference 'IssueRelation.count' do + post :create, :issue_id => 1, + :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''} + end + end + + def test_create_xhr_with_failure + assert_no_difference 'IssueRelation.count' do + xhr :post, :create, :issue_id => 3, :relation => {:issue_to_id => '999', :relation_type => 'relates', :delay => ''} + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + + assert_match /errorExplanation/, response.body + end + + def test_destroy + assert_difference 'IssueRelation.count', -1 do + delete :destroy, :id => '2' + end + end + + def test_destroy_xhr + IssueRelation.create!(:relation_type => IssueRelation::TYPE_RELATES) do |r| + r.issue_from_id = 3 + r.issue_to_id = 1 + end + + assert_difference 'IssueRelation.count', -1 do + xhr :delete, :destroy, :id => '2' + + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + assert_match /relation-2/, response.body + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0d408463a01e894d3f86946e8b5d1925b0335095.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d408463a01e894d3f86946e8b5d1925b0335095.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0d422b89acdac2bb8c5929f11e22f0904e9f370b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d422b89acdac2bb8c5929f11e22f0904e9f370b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0d50a39088b6bd107e54b315b0bf77a7ffa63596.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d50a39088b6bd107e54b315b0bf77a7ffa63596.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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 d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0d6f6e567915dec9c4fe1362362e16f727a01572.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d6f6e567915dec9c4fe1362362e16f727a01572.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,38 @@ +

    <%= l(:label_board_plural) %>

    + + + + + + + + + +<% Board.board_tree(@boards) do |board, level| %> + + + + + + +<% end %> + +
    <%= 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 %><%= board.messages_count %> + <% if board.last_message %> + <%= authoring board.last_message.created_on, board.last_message.author %>
    + <%= link_to_message board.last_message %> + <% end %> +
    + +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_messages => 1, :key => User.current.rss_key} %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> +<% end %> + +<% html_title l(:label_board_plural) %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0dbc51ba6d0f40060a5f81b2175600adf04e0452.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0dbc51ba6d0f40060a5f81b2175600adf04e0452.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,42 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::TokenAuthenticationTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + Setting.login_required = '1' + end + + def teardown + Setting.rest_api_enabled = '0' + Setting.login_required = '0' + end + + # Using the NewsController because it's a simple API. + should_allow_key_based_auth(:get, "/news.xml") + should_allow_key_based_auth(:get, "/news.json") +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0d/0de0220c6abf526fd90b0775dd5b7d3c897e479b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0de0220c6abf526fd90b0775dd5b7d3c897e479b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,10 @@ +class AddTrackerPosition < ActiveRecord::Migration + def self.up + add_column :trackers, :position, :integer, :default => 1 + Tracker.all.each_with_index {|tracker, i| tracker.update_attribute(:position, i+1)} + end + + def self.down + remove_column :trackers, :position + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0e42f308f583a3ce85159cf1c8c287913f2f20ce.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0e42f308f583a3ce85159cf1c8c287913f2f20ce.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,7 @@ +
    +<%= link_to l(:button_edit), edit_issue_path(@issue), :onclick => 'showAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit) if @issue.editable? %> +<%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %> +<%= watcher_link(@issue, User.current) %> +<%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), :class => 'icon icon-copy' if User.current.allowed_to?(:add_issues, @project) %> +<%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %> +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0e54508d3830a714379a5549a38e89ff26176202.svn-base --- a/.svn/pristine/0e/0e54508d3830a714379a5549a38e89ff26176202.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -

    <%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)).html_safe %>

    -

    <%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %>

    - -
    -<% @events_by_day.keys.sort.reverse.each do |day| %> -

    <%= format_activity_day(day) %>

    -
    -<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> -
    - <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %> - <%= format_time(e.event_datetime, false) %> - <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> - <%= link_to format_activity_title(e.event_title), e.event_url %>
    -
    <%= format_activity_description(e.event_description) %> - <%= link_to_user(e.event_author) if e.respond_to?(:event_author) %>
    -<% end -%> -
    -<% end -%> -
    - -<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %> - -
    -<%= link_to_content_update("\xc2\xab " + l(:label_previous), - params.merge(:from => @date_to - @days - 1), - :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %> -
    -
    -<%= link_to_content_update(l(:label_next) + " \xc2\xbb", - params.merge(:from => @date_to + @days - 1), - :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))) unless @date_to >= Date.today %> -
    -  -<% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %> -<% end %> - -<% content_for :header_tags do %> -<%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %> -<% end %> - -<% content_for :sidebar do %> -<%= form_tag({}, :method => :get) do %> -

    <%= l(:label_activity) %>

    -

    <% @activity.event_types.each do |t| %> -<%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> - -
    -<% end %>

    -<% if @project && @project.descendants.active.any? %> - <%= hidden_field_tag 'with_subprojects', 0 %> -

    -<% end %> -<%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %> -

    <%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>

    -<% end %> -<% end %> - -<% html_title(l(:label_activity), @author) -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0e59eac074a412c3150c3135d0a1eec08baa4fba.svn-base --- a/.svn/pristine/0e/0e59eac074a412c3150c3135d0a1eec08baa4fba.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,265 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'issues_controller' - -class IssuesControllerTransactionTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - self.use_transactional_fixtures = false - - def setup - @controller = IssuesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - end - - def test_update_stale_issue_should_not_update_the_issue - issue = Issue.find(2) - @request.session[:user_id] = 2 - - assert_no_difference 'Journal.count' do - assert_no_difference 'TimeEntry.count' do - put :update, - :id => issue.id, - :issue => { - :fixed_version_id => 4, - :notes => 'My notes', - :lock_version => (issue.lock_version - 1) - }, - :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } - end - end - - assert_response :success - assert_template 'edit' - - assert_select 'div.conflict' - assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite' - assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes' - assert_select 'label' do - assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel' - assert_select 'a[href=/issues/2]' - end - end - - def test_update_stale_issue_should_save_attachments - set_tmp_attachments_directory - issue = Issue.find(2) - @request.session[:user_id] = 2 - - assert_no_difference 'Journal.count' do - assert_no_difference 'TimeEntry.count' do - assert_difference 'Attachment.count' do - put :update, - :id => issue.id, - :issue => { - :fixed_version_id => 4, - :notes => 'My notes', - :lock_version => (issue.lock_version - 1) - }, - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}, - :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } - end - end - end - - assert_response :success - assert_template 'edit' - attachment = Attachment.first(:order => 'id DESC') - assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} - assert_tag 'span', :content => /testfile.txt/ - end - - def test_update_stale_issue_without_notes_should_not_show_add_notes_option - issue = Issue.find(2) - @request.session[:user_id] = 2 - - put :update, :id => issue.id, - :issue => { - :fixed_version_id => 4, - :notes => '', - :lock_version => (issue.lock_version - 1) - } - - assert_tag 'div', :attributes => {:class => 'conflict'} - assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'} - assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'} - assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'} - end - - def test_update_stale_issue_should_show_conflicting_journals - @request.session[:user_id] = 2 - - put :update, :id => 1, - :issue => { - :fixed_version_id => 4, - :notes => '', - :lock_version => 2 - }, - :last_journal_id => 1 - - assert_not_nil assigns(:conflict_journals) - assert_equal 1, assigns(:conflict_journals).size - assert_equal 2, assigns(:conflict_journals).first.id - assert_tag 'div', :attributes => {:class => 'conflict'}, - :descendant => {:content => /Some notes with Redmine links/} - end - - def test_update_stale_issue_without_previous_journal_should_show_all_journals - @request.session[:user_id] = 2 - - put :update, :id => 1, - :issue => { - :fixed_version_id => 4, - :notes => '', - :lock_version => 2 - }, - :last_journal_id => '' - - assert_not_nil assigns(:conflict_journals) - assert_equal 2, assigns(:conflict_journals).size - assert_tag 'div', :attributes => {:class => 'conflict'}, - :descendant => {:content => /Some notes with Redmine links/} - assert_tag 'div', :attributes => {:class => 'conflict'}, - :descendant => {:content => /Journal notes/} - end - - def test_update_stale_issue_should_show_private_journals_with_permission_only - journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1) - - @request.session[:user_id] = 2 - put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' - assert_include journal, assigns(:conflict_journals) - - Role.find(1).remove_permission! :view_private_notes - put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' - assert_not_include journal, assigns(:conflict_journals) - end - - def test_update_stale_issue_with_overwrite_conflict_resolution_should_update - @request.session[:user_id] = 2 - - assert_difference 'Journal.count' do - put :update, :id => 1, - :issue => { - :fixed_version_id => 4, - :notes => 'overwrite_conflict_resolution', - :lock_version => 2 - }, - :conflict_resolution => 'overwrite' - end - - assert_response 302 - issue = Issue.find(1) - assert_equal 4, issue.fixed_version_id - journal = Journal.first(:order => 'id DESC') - assert_equal 'overwrite_conflict_resolution', journal.notes - assert journal.details.any? - end - - def test_update_stale_issue_with_add_notes_conflict_resolution_should_update - @request.session[:user_id] = 2 - - assert_difference 'Journal.count' do - put :update, :id => 1, - :issue => { - :fixed_version_id => 4, - :notes => 'add_notes_conflict_resolution', - :lock_version => 2 - }, - :conflict_resolution => 'add_notes' - end - - assert_response 302 - issue = Issue.find(1) - assert_nil issue.fixed_version_id - journal = Journal.first(:order => 'id DESC') - assert_equal 'add_notes_conflict_resolution', journal.notes - assert journal.details.empty? - end - - def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating - @request.session[:user_id] = 2 - - assert_no_difference 'Journal.count' do - put :update, :id => 1, - :issue => { - :fixed_version_id => 4, - :notes => 'add_notes_conflict_resolution', - :lock_version => 2 - }, - :conflict_resolution => 'cancel' - end - - assert_redirected_to '/issues/1' - issue = Issue.find(1) - assert_nil issue.fixed_version_id - end - - def test_put_update_with_spent_time_and_failure_should_not_add_spent_time - @request.session[:user_id] = 2 - - assert_no_difference('TimeEntry.count') do - put :update, - :id => 1, - :issue => { :subject => '' }, - :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id } - assert_response :success - end - - assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5' - assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added' - assert_select 'select[name=?]', 'time_entry[activity_id]' do - assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id - end - end - - def test_index_should_rescue_invalid_sql_query - Query.any_instance.stubs(:statement).returns("INVALID STATEMENT") - - get :index - assert_response 500 - assert_tag 'p', :content => /An error occurred/ - assert_nil session[:query] - assert_nil session[:issues_index_sort] - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0e6d154c5ead84b5d126f01a8ce10bcd16d83cbd.svn-base --- a/.svn/pristine/0e/0e6d154c5ead84b5d126f01a8ce10bcd16d83cbd.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 WikisController < ApplicationController - menu_item :settings - before_filter :find_project, :authorize - - # Create or update a project's wiki - def edit - @wiki = @project.wiki || Wiki.new(:project => @project) - @wiki.safe_attributes = params[:wiki] - @wiki.save if request.post? - end - - # Delete a project's wiki - def destroy - if request.post? && params[:confirm] && @project.wiki - @project.wiki.destroy - redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki' - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0eb716c2e3251d0a363271012989e30efeb34f17.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0eb716c2e3251d0a363271012989e30efeb34f17.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,3 @@ +<%= form_tag(signout_path) do %> +

    <%= submit_tag l(:label_logout) %>

    +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0ed2a8155e8187c04d224caafbc3970f5a91d733.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0ed2a8155e8187c04d224caafbc3970f5a91d733.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,198 @@ +

    <%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %>

    + +<% if @saved_issues && @unsaved_issues.present? %> +
    + + <%= l(:notice_failed_to_save_issues, + :count => @unsaved_issues.size, + :total => @saved_issues.size, + :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %> + +
      + <% bulk_edit_error_messages(@unsaved_issues).each do |message| %> +
    • <%= message %>
    • + <% end %> +
    +
    +<% end %> + +
      +<% @issues.each do |issue| %> + <%= content_tag 'li', link_to_issue(issue) %> +<% end %> +
    + +<%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %> +
    +
    +<%= l(:label_change_properties) %> + +
    +<% if @allowed_projects.present? %> +

    + + <%= select_tag('issue[project_id]', + content_tag('option', l(:label_no_change_option), :value => '') + + project_tree_options_for_select(@allowed_projects, :selected => @target_project), + :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %> +

    +<% end %> +

    + + <%= select_tag('issue[tracker_id]', + content_tag('option', l(:label_no_change_option), :value => '') + + options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %> +

    +<% if @available_statuses.any? %> +

    + + <%= select_tag('issue[status_id]', + content_tag('option', l(:label_no_change_option), :value => '') + + options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %> +

    +<% end %> + +<% if @safe_attributes.include?('priority_id') -%> +

    + + <%= select_tag('issue[priority_id]', + content_tag('option', l(:label_no_change_option), :value => '') + + options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %> +

    +<% end %> + +<% if @safe_attributes.include?('assigned_to_id') -%> +

    + + <%= select_tag('issue[assigned_to_id]', + content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) + + principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %> +

    +<% end %> + +<% if @safe_attributes.include?('category_id') -%> +

    + + <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) + + options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %> +

    +<% end %> + +<% if @safe_attributes.include?('fixed_version_id') -%> +

    + + <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) + + version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %> +

    +<% end %> + +<% @custom_fields.each do |custom_field| %> +

    + + <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects, @issue_params[:custom_field_values][custom_field.id.to_s]) %> +

    +<% end %> + +<% if @copy && @attachments_present %> +<%= hidden_field_tag 'copy_attachments', '0' %> +

    + + <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %> +

    +<% end %> + +<% if @copy && @subtasks_present %> +<%= hidden_field_tag 'copy_subtasks', '0' %> +

    + + <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %> +

    +<% end %> + +<%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %> +
    + +
    +<% if @safe_attributes.include?('is_private') %> +

    + + <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) + + content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %> +

    +<% end %> + +<% if @safe_attributes.include?('parent_issue_id') && @project %> +

    + + <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %> + +

    +<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project)}')" %> +<% end %> + +<% if @safe_attributes.include?('start_date') %> +

    + + <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %> + +

    +<% end %> + +<% if @safe_attributes.include?('due_date') %> +

    + + <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %> + +

    +<% end %> + +<% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %> +

    + + <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }, @issue_params[:done_ratio]) %> +

    +<% end %> +
    +
    + +
    +<%= l(:field_notes) %> +<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'notes' %> +
    +
    + +

    + <% 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 d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0ef99b09fb86deecc2f7d49dd96b01429edf06cb.svn-base Binary file .svn/pristine/0e/0ef99b09fb86deecc2f7d49dd96b01429edf06cb.svn-base has changed diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0e/0efedaea2e714f5dbe3b6a1b0cd102c208875d70.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0efedaea2e714f5dbe3b6a1b0cd102c208875d70.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -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ï¼‰ã€æ•°å­—ã€ç ´æŠ˜å·ï¼ˆ-)和下划线(_)å¯ä»¥ä½¿ç”¨ã€‚
    一旦ä¿å­˜ï¼Œæ ‡è¯†æ— æ³•修改。 + field_multiple: 多é‡å–值 + setting_commit_cross_project_ref: å…许引用/ä¿®å¤æ‰€æœ‰å…¶ä»–项目的问题 + text_issue_conflict_resolution_add_notes: æ·»åŠ è¯´æ˜Žå¹¶å–æ¶ˆæˆ‘çš„å…¶ä»–å˜æ›´å¤„ç†ã€‚ + text_issue_conflict_resolution_overwrite: ç›´æŽ¥å¥—ç”¨æˆ‘çš„å˜æ›´ (先å‰çš„说明将被ä¿ç•™ï¼Œä½†æ˜¯æŸäº›å˜æ›´å†…容å¯èƒ½ä¼šè¢«è¦†ç›–) + notice_issue_update_conflict: 当您正在编辑这个问题的时候,它已ç»è¢«å…¶ä»–人抢先一步更新过了。 + text_issue_conflict_resolution_cancel: å–æ¶ˆæˆ‘æ‰€æœ‰çš„å˜æ›´å¹¶é‡æ–°åˆ·æ–°æ˜¾ç¤º %{link} 。 + permission_manage_related_issues: ç›¸å…³é—®é¢˜ç®¡ç† + field_auth_source_ldap_filter: LDAP 过滤器 + label_search_for_watchers: é€šè¿‡æŸ¥æ‰¾æ–¹å¼æ·»åŠ è·Ÿè¸ªè€… + notice_account_deleted: 您的账å·å·²è¢«æ°¸ä¹…删除(账å·å·²æ— æ³•æ¢å¤ï¼‰ã€‚ + setting_unsubscribe: å…许用户退订 + button_delete_my_account: åˆ é™¤æˆ‘çš„è´¦å· + text_account_destroy_confirmation: |- + 确定继续处ç†ï¼Ÿ + 您的账å·ä¸€æ—¦åˆ é™¤ï¼Œå°†æ— æ³•冿¬¡æ¿€æ´»ä½¿ç”¨ã€‚ + error_session_expired: 您的会è¯å·²è¿‡æœŸã€‚è¯·é‡æ–°ç™»é™†ã€‚ + text_session_expiration_settings: "警告: 更改这些设置将会使包括你在内的当å‰ä¼šè¯å¤±æ•ˆã€‚" + setting_session_lifetime: ä¼šè¯æœ€å¤§æœ‰æ•ˆæ—¶é—´ + setting_session_timeout: 会è¯é—²ç½®è¶…æ—¶ + label_session_expiration: 会è¯è¿‡æœŸ + permission_close_project: 关闭/é‡å¼€é¡¹ç›® + label_show_closed_projects: 查看已关闭的项目 + button_close: 关闭 + button_reopen: é‡å¼€ + project_status_active: 已激活 + project_status_closed: 已关闭 + project_status_archived: 已存档 + text_project_closed: 当å‰é¡¹ç›®å·²è¢«å…³é—­ã€‚当å‰é¡¹ç›®åªè¯»ã€‚ + notice_user_successful_create: 用户 %{id} 已创建。 + field_core_fields: 标准字段 + field_timeout: è¶…æ—¶ (ç§’) + setting_thumbnails_enabled: 显示附件略缩图 + setting_thumbnails_size: 略缩图尺寸 (åƒç´ ) + label_status_transitions: 状æ€è½¬æ¢ + label_fields_permissions: 字段æƒé™ + label_readonly: åªè¯» + label_required: å¿…å¡« + text_repository_identifier_info: ä»…å°å†™å­—æ¯ï¼ˆa-zï¼‰ã€æ•°å­—ã€ç ´æŠ˜å·ï¼ˆ-)和下划线(_)å¯ä»¥ä½¿ç”¨ã€‚
    一旦ä¿å­˜ï¼Œæ ‡è¯†æ— æ³•修改。 + field_board_parent: çˆ¶è®ºå› + label_attribute_of_project: 项目 %{name} + label_attribute_of_author: 作者 %{name} + label_attribute_of_assigned_to: 分é…ç»™ %{name} + label_attribute_of_fixed_version: 目标版本 %{name} + label_copy_subtasks: å¤åˆ¶å­ä»»åŠ¡ + label_copied_to: å¤åˆ¶åˆ° + label_copied_from: å¤åˆ¶äºŽ + label_any_issues_in_project: 项目内任æ„问题 + label_any_issues_not_in_project: 项目外任æ„问题 + field_private_notes: ç§æœ‰æ³¨è§£ + permission_view_private_notes: æŸ¥çœ‹ç§æœ‰æ³¨è§£ + permission_set_notes_private: è®¾ç½®ä¸ºç§æœ‰æ³¨è§£ + label_no_issues_in_project: 项目内无相关问题 + label_any: 全部 + label_last_n_weeks: 上 %{count} å‘¨å‰ + setting_cross_project_subtasks: 支æŒè·¨é¡¹ç›®å­ä»»åŠ¡ + label_cross_project_descendants: 与å­é¡¹ç›®å…±äº« + label_cross_project_tree: 与项目树共享 + label_cross_project_hierarchy: 与项目继承层次共享 + label_cross_project_system: 与所有项目共享 + button_hide: éšè— + setting_non_working_week_days: éžå·¥ä½œæ—¥ + label_in_the_next_days: 在未æ¥å‡ å¤©ä¹‹å†… + label_in_the_past_days: 在过去几天之内 + label_attribute_of_user: 用户是 %{name} + text_turning_multiple_off: 如果您åœç”¨å¤šé‡å€¼è®¾å®šï¼Œé‡å¤çš„值将被移除,以使æ¯ä¸ªé¡¹ç›®ä»…ä¿ç•™ä¸€ä¸ªå€¼ + label_attribute_of_issue: 问题是 %{name} + permission_add_documents: 添加文档 + permission_edit_documents: 编辑文档 + permission_delete_documents: 删除文档 + label_gantt_progress_line: 进度线 + setting_jsonp_enabled: å¯ç”¨JSONPæ”¯æŒ + field_inherit_members: 继承父项目æˆå‘˜ + field_closed_on: ç»“æŸæ—¥æœŸ + field_generate_password: 生æˆå¯†ç  + setting_default_projects_tracker_ids: 新建项目默认跟踪标签 + label_total_time: åˆè®¡ + notice_account_not_activated_yet: 您的账å·å°šæœªæ¿€æ´». 若您è¦é‡æ–°æ”¶å–激活邮件, 请å•击此链接. + notice_account_locked: 您的å¸å·å·²è¢«é”定 + label_hidden: éšè— + label_visibility_private: 仅对我å¯è§ + label_visibility_roles: 仅对选å–角色å¯è§ + label_visibility_public: 对任何人å¯è§ + field_must_change_passwd: ä¸‹æ¬¡ç™»å½•æ—¶å¿…é¡»ä¿®æ”¹å¯†ç  + notice_new_password_must_be_different: 新密ç å¿…须和旧密ç ä¸åŒ + setting_mail_handler_excluded_filenames: 移除符åˆä¸‹åˆ—å称的附件 + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0f/0f41eec6f8bdcfb1c9cd2c332bcf208f96828f37.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0f/0f41eec6f8bdcfb1c9cd2c332bcf208f96828f37.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,322 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssuesCustomFieldsVisibilityTest < ActionController::TestCase + tests IssuesController + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issue_statuses, + :trackers, + :projects_trackers, + :enabled_modules, + :enumerations, + :workflows + + def setup + CustomField.delete_all + Issue.delete_all + field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all} + @fields = [] + @fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true))) + @fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2]))) + @fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3]))) + @issue = Issue.generate!( + :author_id => 1, + :project_id => 1, + :tracker_id => 1, + :custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'} + ) + + @user_with_role_on_other_project = User.generate! + User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3)) + + @users_to_test = { + User.find(1) => [@field1, @field2, @field3], + User.find(3) => [@field1, @field2], + @user_with_role_on_other_project => [@field1], # should see field1 only on Project 1 + User.generate! => [@field1], + User.anonymous => [@field1] + } + + Member.where(:project_id => 1).each do |member| + member.destroy unless @users_to_test.keys.include?(member.principal) + end + end + + def test_show_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :show, :id => @issue.id + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}" + else + assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}" + end + end + end + end + + def test_show_should_show_visible_custom_fields_only_in_api + @users_to_test.each do |user, fields| + with_settings :rest_api_enabled => '1' do + get :show, :id => @issue.id, :format => 'xml', :include => 'custom_fields', :key => user.api_key + end + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select "custom_field[id=#{field.id}] value", {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} in API" + else + assert_select "custom_field[id=#{field.id}] value", {:text => "Value#{i}", :count => 0}, "User #{user.id} was not able to view #{field.name} in API" + end + end + end + end + + def test_show_should_show_visible_custom_fields_only_in_history + @issue.init_journal(User.find(1)) + @issue.custom_field_values = {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'} + @issue.save! + + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :show, :id => @issue.id + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'ul.details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change" + else + assert_select 'ul.details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change" + end + end + end + end + + def test_show_should_show_visible_custom_fields_only_in_history_api + @issue.init_journal(User.find(1)) + @issue.custom_field_values = {@field1.id => 'NewValue0', @field2.id => 'NewValue1', @field3.id => 'NewValue2'} + @issue.save! + + @users_to_test.each do |user, fields| + with_settings :rest_api_enabled => '1' do + get :show, :id => @issue.id, :format => 'xml', :include => 'journals', :key => user.api_key + end + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'details old_value', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change in API" + else + assert_select 'details old_value', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change in API" + end + end + end + end + + def test_edit_should_show_visible_custom_fields_only + Role.anonymous.add_permission! :edit_issues + + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :edit, :id => @issue.id + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'input[value=?]', "Value#{i}", 1, "User #{user.id} was not able to edit #{field.name}" + else + assert_select 'input[value=?]', "Value#{i}", 0, "User #{user.id} was able to edit #{field.name}" + end + end + end + end + + def test_update_should_update_visible_custom_fields_only + Role.anonymous.add_permission! :edit_issues + + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + put :update, :id => @issue.id, + :issue => {:custom_field_values => { + @field1.id.to_s => "User#{user.id}Value0", + @field2.id.to_s => "User#{user.id}Value1", + @field3.id.to_s => "User#{user.id}Value2", + }} + @issue.reload + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_equal "User#{user.id}Value#{i}", @issue.custom_field_value(field), "User #{user.id} was not able to update #{field.name}" + else + assert_not_equal "User#{user.id}Value#{i}", @issue.custom_field_value(field), "User #{user.id} was able to update #{field.name}" + end + end + end + end + + def test_index_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :index, :c => (["subject"] + @fields.map{|f| "cf_#{f.id}"}) + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}" + else + assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}" + end + end + end + end + + def test_index_as_csv_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :index, :c => (["subject"] + @fields.map{|f| "cf_#{f.id}"}), :format => 'csv' + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_include "Value#{i}", response.body, "User #{user.id} was not able to view #{field.name} in CSV" + else + assert_not_include "Value#{i}", response.body, "User #{user.id} was able to view #{field.name} in CSV" + end + end + end + end + + def test_index_with_partial_custom_field_visibility + Issue.delete_all + p1 = Project.generate! + p2 = Project.generate! + user = User.generate! + User.add_to_project(user, p1, Role.where(:id => [1, 3]).all) + User.add_to_project(user, p2, Role.where(:id => 3).all) + Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueA'}) + Issue.generate!(:project => p2, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueB'}) + Issue.generate!(:project => p1, :tracker_id => 1, :custom_field_values => {@field2.id => 'ValueC'}) + + @request.session[:user_id] = user.id + get :index, :c => ["subject", "cf_#{@field2.id}"] + assert_select 'td', :text => 'ValueA' + assert_select 'td', :text => 'ValueB', :count => 0 + assert_select 'td', :text => 'ValueC' + + get :index, :sort => "cf_#{@field2.id}" + # ValueB is not visible to user and ignored while sorting + assert_equal %w(ValueB ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)} + + get :index, :set_filter => '1', "cf_#{@field2.id}" => '*' + assert_equal %w(ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)} + + CustomField.update_all(:field_format => 'list') + get :index, :group => "cf_#{@field2.id}" + assert_equal %w(ValueA ValueC), assigns(:issues).map{|i| i.custom_field_value(@field2)} + end + + def test_create_should_send_notifications_according_custom_fields_visibility + # anonymous user is never notified + users_to_test = @users_to_test.reject {|k,v| k.anonymous?} + + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 1 + with_settings :bcc_recipients => '1' do + assert_difference 'Issue.count' do + post :create, + :project_id => 1, + :issue => { + :tracker_id => 1, + :status_id => 1, + :subject => 'New issue', + :priority_id => 5, + :custom_field_values => {@field1.id.to_s => 'Value0', @field2.id.to_s => 'Value1', @field3.id.to_s => 'Value2'}, + :watcher_user_ids => users_to_test.keys.map(&:id) + } + assert_response 302 + end + end + assert_equal users_to_test.values.uniq.size, ActionMailer::Base.deliveries.size + # tests that each user receives 1 email with the custom fields he is allowed to see only + users_to_test.each do |user, fields| + mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail} + assert_equal 1, mails.size + mail = mails.first + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_mail_body_match "Value#{i}", mail, "User #{user.id} was not able to view #{field.name} in notification" + else + assert_mail_body_no_match "Value#{i}", mail, "User #{user.id} was able to view #{field.name} in notification" + end + end + end + end + + def test_update_should_send_notifications_according_custom_fields_visibility + # anonymous user is never notified + users_to_test = @users_to_test.reject {|k,v| k.anonymous?} + + users_to_test.keys.each do |user| + Watcher.create!(:user => user, :watchable => @issue) + end + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 1 + with_settings :bcc_recipients => '1' do + put :update, + :id => @issue.id, + :issue => { + :custom_field_values => {@field1.id.to_s => 'NewValue0', @field2.id.to_s => 'NewValue1', @field3.id.to_s => 'NewValue2'} + } + assert_response 302 + end + assert_equal users_to_test.values.uniq.size, ActionMailer::Base.deliveries.size + # tests that each user receives 1 email with the custom fields he is allowed to see only + users_to_test.each do |user, fields| + mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail} + assert_equal 1, mails.size + mail = mails.first + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_mail_body_match "Value#{i}", mail, "User #{user.id} was not able to view #{field.name} in notification" + else + assert_mail_body_no_match "Value#{i}", mail, "User #{user.id} was able to view #{field.name} in notification" + end + end + end + end + + def test_updating_hidden_custom_fields_only_should_not_notifiy_user + # anonymous user is never notified + users_to_test = @users_to_test.reject {|k,v| k.anonymous?} + + users_to_test.keys.each do |user| + Watcher.create!(:user => user, :watchable => @issue) + end + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 1 + with_settings :bcc_recipients => '1' do + put :update, + :id => @issue.id, + :issue => { + :custom_field_values => {@field2.id.to_s => 'NewValue1', @field3.id.to_s => 'NewValue2'} + } + assert_response 302 + end + users_to_test.each do |user, fields| + mails = ActionMailer::Base.deliveries.select {|m| m.bcc.include? user.mail} + if (fields & [@field2, @field3]).any? + assert_equal 1, mails.size, "User #{user.id} was not notified" + else + assert_equal 0, mails.size, "User #{user.id} was notified" + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0f/0f6a1a5f91b1625dba02a90214d3f1e2572096f8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0f/0f6a1a5f91b1625dba02a90214d3f1e2572096f8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,7 @@ +--- a.txt 2013-04-05 14:19:39.000000000 +0900 ++++ b.txt 2013-04-05 14:19:51.000000000 +0900 +@@ -1,3 +1,3 @@ + aaaa +-日本 ++日本語 + bbbb diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0f/0f9b26db420557a0a5b8bf781aba4ca50d51d3d0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0f/0f9b26db420557a0a5b8bf781aba4ca50d51d3d0.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,80 @@ + + + + +<%=h html_title %> + + +<%= csrf_meta_tag %> +<%= favicon %> +<%= stylesheet_link_tag 'jquery/jquery-ui-1.9.2', 'application', :media => 'all' %> +<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %> +<%= javascript_heads %> +<%= heads_for_theme %> +<%= call_hook :view_layouts_base_html_head %> + +<%= yield :header_tags -%> + + +
    +
    +
    +
    +
    + <%= render_menu :account_menu -%> +
    + <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %> + <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%> +
    + + + + +
    + + + + + +
    +
    +<%= call_hook :view_layouts_base_body_bottom %> + + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/0f/0fe8309158c952d8ecd963cac89b3329eaf84e52.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0f/0fe8309158c952d8ecd963cac89b3329eaf84e52.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,89 @@ +<%= error_messages_for 'member' %> +<% roles = Role.find_all_givable + members = @project.member_principals.includes(:member_roles, :roles, :principal).all.sort %> + +
    +<% if members.any? %> + + + + + + <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %> + + + <% members.each do |member| %> + <% next if member.new_record? %> + + + + + <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %> + +<% end; reset_cycle %> + +
    <%= l(:label_user) %> / <%= l(:label_group) %><%= l(:label_role_plural) %>
    <%= link_to_user member.principal %> + <%= member.roles.sort.collect(&:to_s).join(', ') %> + <%= form_for(member, + {:as => :membership, :remote => true, + :url => membership_path(member), + :method => :put, + :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }} + ) do |f| %> +

    + <% roles.each do |role| %> +
    + <% end %> +

    + <%= hidden_field_tag 'membership[role_ids][]', '' %> +

    + <%= submit_tag l(:button_save), :class => "small" %> + <%= link_to_function(l(:button_cancel), + "$('#member-#{member.id}-roles').show(); $('#member-#{member.id}-roles-form').hide(); return false;") %> +

    + <% end %> +
    + <%= link_to_function l(:button_edit), + "$('#member-#{member.id}-roles').hide(); $('#member-#{member.id}-roles-form').show(); return false;", + :class => 'icon icon-edit' %> + <%= delete_link membership_path(member), + :remote => true, + :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %> +
    +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> +
    + +
    +<% if roles.any? %> + <%= form_for(@member, + {:as => :membership, :url => project_memberships_path(@project), + :remote => true, :method => :post}) do |f| %> +
    + <%=l(:label_member_new)%> +

    + <%= label_tag("principal_search", l(:label_principal_search)) %> + <%= text_field_tag('principal_search', nil) %> +

    + <%= javascript_tag "observeSearchfield('principal_search', null, '#{ escape_javascript autocomplete_project_memberships_path(@project, :format => 'js') }')" %> +
    + <%= render_principals_for_new_members(@project) %> +
    +

    + <%= l(:label_role_plural) %>: + <% roles.each do |role| %> + + <% end %> +

    +

    <%= submit_tag l(:button_add), :id => 'member-add-submit' %>

    +
    + <% end %> +<% end %> +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/10/102e022c085717aac03a1b315b6cdb177e206a26.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/10/102e022c085717aac03a1b315b6cdb177e206a26.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,92 @@ +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 + if scms = ENV['SCMS'] + scms.split(',').each do |scm| + Rake::Task["test:scm:setup:#{scm}"].invoke + end + else + Rake::Task["test:scm:setup:all"].invoke + end + Rake::Task["test:scm:update"].invoke + end + + desc "Build Redmine" + task :build do + if test_suite = ENV['TEST_SUITE'] + Rake::Task["test:#{test_suite}"].invoke + else + Rake::Task["test"].invoke + end + # 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', + 'encoding' => 'utf8'} + if ENV['RUN_ON_NOT_OFFICIAL'] + dev_conf['username'] = 'root' + else + dev_conf['username'] = 'jenkins' + dev_conf['password'] = 'jenkins' + end + test_conf = dev_conf.merge('database' => test_db_name) + when 'postgresql' + dev_conf = {'adapter' => 'postgresql', 'database' => dev_db_name, + 'host' => 'localhost'} + if ENV['RUN_ON_NOT_OFFICIAL'] + dev_conf['username'] = 'postgres' + else + dev_conf['username'] = 'jenkins' + dev_conf['password'] = 'jenkins' + end + 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 d98d22a98252 -r fb9a13467253 .svn/pristine/10/105399d73cdd64dbe994d874bdf78e986ecd631d.svn-base --- a/.svn/pristine/10/105399d73cdd64dbe994d874bdf78e986ecd631d.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 TrackersController < ApplicationController - layout 'admin' - - before_filter :require_admin, :except => :index - before_filter :require_admin_or_api_request, :only => :index - accept_api_auth :index - - def index - respond_to do |format| - format.html { - @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position' - render :action => "index", :layout => false if request.xhr? - } - format.api { - @trackers = Tracker.sorted.all - } - end - end - - def new - @tracker ||= Tracker.new(params[:tracker]) - @trackers = Tracker.find :all, :order => 'position' - @projects = Project.find(:all) - end - - def create - @tracker = Tracker.new(params[:tracker]) - if request.post? and @tracker.save - # workflow copy - if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) - @tracker.workflow_rules.copy(copy_from) - end - flash[:notice] = l(:notice_successful_create) - redirect_to :action => 'index' - return - end - new - render :action => 'new' - end - - def edit - @tracker ||= Tracker.find(params[:id]) - @projects = Project.find(:all) - end - - def update - @tracker = Tracker.find(params[:id]) - if request.put? and @tracker.update_attributes(params[:tracker]) - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'index' - return - end - edit - render :action => 'edit' - end - - def destroy - @tracker = Tracker.find(params[:id]) - unless @tracker.issues.empty? - flash[:error] = l(:error_can_not_delete_tracker) - else - @tracker.destroy - end - redirect_to :action => 'index' - end - - def fields - if request.post? && params[:trackers] - params[:trackers].each do |tracker_id, tracker_params| - tracker = Tracker.find_by_id(tracker_id) - if tracker - tracker.core_fields = tracker_params[:core_fields] - tracker.custom_field_ids = tracker_params[:custom_field_ids] - tracker.save - end - end - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'fields' - return - end - @trackers = Tracker.sorted.all - @custom_fields = IssueCustomField.all.sort - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/10/106f50e02eb006cdb6936c9a24402da5b33e6c91.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/10/106f50e02eb006cdb6936c9a24402da5b33e6c91.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,3 @@ +

    <%= f.text_field(:default_value, :size => 10) %>

    +<%= calendar_for('custom_field_default_value') %> +

    <%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %>

    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/10/10904ccde85ccf401e73eeaa430c91cf12bd3a73.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/10/10904ccde85ccf401e73eeaa430c91cf12bd3a73.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,351 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 AccountController < ApplicationController + helper :custom_fields + include CustomFieldsHelper + + # prevents login action to be filtered by check_if_login_required application scope filter + skip_before_filter :check_if_login_required, :check_password_change + + # Overrides ApplicationController#verify_authenticity_token to disable + # token verification on openid callbacks + def verify_authenticity_token + unless using_open_id? + super + end + end + + # Login request and validation + def login + if request.get? + if User.current.logged? + redirect_back_or_default home_url, :referer => true + end + else + authenticate_user + end + rescue AuthSourceException => e + logger.error "An error occured when authenticating #{params[:username]}: #{e.message}" + render_error :message => e.message + end + + # Log out current user and redirect to welcome page + def logout + if User.current.anonymous? + redirect_to home_url + elsif request.post? + logout_user + redirect_to home_url + end + # display the logout form + end + + # Lets user choose a new password + def lost_password + (redirect_to(home_url); return) unless Setting.lost_password? + if params[:token] + @token = Token.find_token("recovery", params[:token].to_s) + if @token.nil? || @token.expired? + redirect_to home_url + return + end + @user = @token.user + unless @user && @user.active? + redirect_to home_url + return + end + if request.post? + @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] + if @user.save + @token.destroy + flash[:notice] = l(:notice_account_password_updated) + redirect_to signin_path + return + end + end + render :template => "account/password_recovery" + return + else + if request.post? + user = User.find_by_mail(params[:mail].to_s) + # user not found + unless user + flash.now[:error] = l(:notice_account_unknown_email) + return + end + unless user.active? + handle_inactive_user(user, lost_password_path) + return + end + # user cannot change its password + unless user.change_password_allowed? + flash.now[:error] = l(:notice_can_t_change_password) + return + end + # create a new token for password recovery + token = Token.new(:user => user, :action => "recovery") + if token.save + Mailer.lost_password(token).deliver + flash[:notice] = l(:notice_account_lost_email_sent) + redirect_to signin_path + return + end + end + end + end + + # User self-registration + def register + (redirect_to(home_url); return) unless Setting.self_registration? || session[:auth_source_registration] + if request.get? + session[:auth_source_registration] = nil + @user = User.new(:language => current_language.to_s) + else + user_params = params[:user] || {} + @user = User.new + @user.safe_attributes = user_params + @user.admin = false + @user.register + if session[:auth_source_registration] + @user.activate + @user.login = session[:auth_source_registration][:login] + @user.auth_source_id = session[:auth_source_registration][:auth_source_id] + if @user.save + session[:auth_source_registration] = nil + self.logged_user = @user + flash[:notice] = l(:notice_account_activated) + redirect_to my_account_path + end + else + @user.login = params[:user][:login] + unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank? + @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation] + end + + case Setting.self_registration + when '1' + register_by_email_activation(@user) + when '3' + register_automatically(@user) + else + register_manually_by_administrator(@user) + end + end + end + end + + # Token based account activation + def activate + (redirect_to(home_url); return) unless Setting.self_registration? && params[:token].present? + token = Token.find_token('register', params[:token].to_s) + (redirect_to(home_url); return) unless token and !token.expired? + user = token.user + (redirect_to(home_url); return) unless user.registered? + user.activate + if user.save + token.destroy + flash[:notice] = l(:notice_account_activated) + end + redirect_to signin_path + end + + # Sends a new account activation email + def activation_email + if session[:registered_user_id] && Setting.self_registration == '1' + user_id = session.delete(:registered_user_id).to_i + user = User.find_by_id(user_id) + if user && user.registered? + register_by_email_activation(user) + return + end + end + redirect_to(home_url) + end + + private + + def authenticate_user + if Setting.openid? && using_open_id? + open_id_authenticate(params[:openid_url]) + else + password_authentication + end + end + + def password_authentication + user = User.try_to_login(params[:username], params[:password], false) + + if user.nil? + invalid_credentials + elsif user.new_record? + onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id }) + else + # Valid user + if user.active? + successful_authentication(user) + else + handle_inactive_user(user) + end + end + end + + def open_id_authenticate(openid_url) + back_url = signin_url(:autologin => params[:autologin]) + authenticate_with_open_id( + openid_url, :required => [:nickname, :fullname, :email], + :return_to => back_url, :method => :post + ) do |result, identity_url, registration| + if result.successful? + user = User.find_or_initialize_by_identity_url(identity_url) + if user.new_record? + # Self-registration off + (redirect_to(home_url); return) unless Setting.self_registration? + # Create on the fly + user.login = registration['nickname'] unless registration['nickname'].nil? + user.mail = registration['email'] unless registration['email'].nil? + user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil? + user.random_password + user.register + case Setting.self_registration + when '1' + register_by_email_activation(user) do + onthefly_creation_failed(user) + end + when '3' + register_automatically(user) do + onthefly_creation_failed(user) + end + else + register_manually_by_administrator(user) do + onthefly_creation_failed(user) + end + end + else + # Existing record + if user.active? + successful_authentication(user) + else + handle_inactive_user(user) + end + end + end + end + end + + def successful_authentication(user) + logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}" + # Valid user + self.logged_user = user + # generate a key and set cookie if autologin + if params[:autologin] && Setting.autologin? + set_autologin_cookie(user) + end + call_hook(:controller_account_success_authentication_after, {:user => user }) + redirect_back_or_default my_page_path + end + + def set_autologin_cookie(user) + token = Token.create(:user => user, :action => 'autologin') + cookie_options = { + :value => token.value, + :expires => 1.year.from_now, + :path => (Redmine::Configuration['autologin_cookie_path'] || '/'), + :secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false), + :httponly => true + } + cookies[autologin_cookie_name] = cookie_options + end + + # Onthefly creation failed, display the registration form to fill/fix attributes + def onthefly_creation_failed(user, auth_source_options = { }) + @user = user + session[:auth_source_registration] = auth_source_options unless auth_source_options.empty? + render :action => 'register' + end + + def invalid_credentials + logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}" + flash.now[:error] = l(:notice_account_invalid_creditentials) + end + + # Register a user for email activation. + # + # Pass a block for behavior when a user fails to save + def register_by_email_activation(user, &block) + token = Token.new(:user => user, :action => "register") + if user.save and token.save + Mailer.register(token).deliver + flash[:notice] = l(:notice_account_register_done, :email => user.mail) + redirect_to signin_path + else + yield if block_given? + end + end + + # Automatically register a user + # + # Pass a block for behavior when a user fails to save + def register_automatically(user, &block) + # Automatic activation + user.activate + user.last_login_on = Time.now + if user.save + self.logged_user = user + flash[:notice] = l(:notice_account_activated) + redirect_to my_account_path + else + yield if block_given? + end + end + + # Manual activation by the administrator + # + # Pass a block for behavior when a user fails to save + def register_manually_by_administrator(user, &block) + if user.save + # Sends an email to the administrators + Mailer.account_activation_request(user).deliver + account_pending(user) + else + yield if block_given? + end + end + + def handle_inactive_user(user, redirect_path=signin_path) + if user.registered? + account_pending(user, redirect_path) + else + account_locked(user, redirect_path) + end + end + + def account_pending(user, redirect_path=signin_path) + if Setting.self_registration == '1' + flash[:error] = l(:notice_account_not_activated_yet, :url => activation_email_path) + session[:registered_user_id] = user.id + else + flash[:error] = l(:notice_account_pending) + end + redirect_to redirect_path + end + + def account_locked(user, redirect_path=signin_path) + flash[:error] = l(:notice_account_locked) + redirect_to redirect_path + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/10/10cf6c62a2177b6e4b8375c93a35de2b98eadb96.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/10/10cf6c62a2177b6e4b8375c93a35de2b98eadb96.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,290 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 Version < ActiveRecord::Base + include Redmine::SafeAttributes + after_update :update_issues_from_sharing_change + belongs_to :project + has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify + acts_as_customizable + acts_as_attachable :view_permission => :view_files, + :delete_permission => :manage_files + + VERSION_STATUSES = %w(open locked closed) + VERSION_SHARINGS = %w(none descendants hierarchy tree system) + + validates_presence_of :name + validates_uniqueness_of :name, :scope => [:project_id] + validates_length_of :name, :maximum => 60 + validates :effective_date, :date => true + validates_inclusion_of :status, :in => VERSION_STATUSES + validates_inclusion_of :sharing, :in => VERSION_SHARINGS + + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + scope :open, lambda { where(:status => 'open') } + scope :visible, lambda {|*args| + includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues)) + } + + safe_attributes 'name', + 'description', + 'effective_date', + 'due_date', + 'wiki_page_title', + 'status', + 'sharing', + 'custom_field_values', + 'custom_fields' + + # Returns true if +user+ or current user is allowed to view the version + def visible?(user=User.current) + user.allowed_to?(:view_issues, self.project) + end + + # Version files have same visibility as project files + def attachments_visible?(*args) + project.present? && project.attachments_visible?(*args) + end + + def start_date + @start_date ||= fixed_issues.minimum('start_date') + end + + def due_date + effective_date + end + + def due_date=(arg) + self.effective_date=(arg) + end + + # Returns the total estimated time for this version + # (sum of leaves estimated_hours) + def estimated_hours + @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f + end + + # Returns the total reported time for this version + def spent_hours + @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f + end + + def closed? + status == 'closed' + end + + def open? + status == 'open' + end + + # Returns true if the version is completed: due date reached and no open issues + def completed? + effective_date && (effective_date < Date.today) && (open_issues_count == 0) + end + + def behind_schedule? + if completed_percent == 100 + return false + elsif due_date && start_date + done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor + return done_date <= Date.today + else + false # No issues so it's not late + end + end + + # Returns the completion percentage of this version based on the amount of open/closed issues + # and the time spent on the open issues. + def completed_percent + if issues_count == 0 + 0 + elsif open_issues_count == 0 + 100 + else + issues_progress(false) + issues_progress(true) + end + end + + # TODO: remove in Redmine 3.0 + def completed_pourcent + ActiveSupport::Deprecation.warn "Version#completed_pourcent is deprecated and will be removed in Redmine 3.0. Please use #completed_percent instead." + completed_percent + end + + # Returns the percentage of issues that have been marked as 'closed'. + def closed_percent + if issues_count == 0 + 0 + else + issues_progress(false) + end + end + + # TODO: remove in Redmine 3.0 + def closed_pourcent + ActiveSupport::Deprecation.warn "Version#closed_pourcent is deprecated and will be removed in Redmine 3.0. Please use #closed_percent instead." + closed_percent + end + + # Returns true if the version is overdue: due date reached and some open issues + def overdue? + effective_date && (effective_date < Date.today) && (open_issues_count > 0) + end + + # Returns assigned issues count + def issues_count + load_issue_counts + @issue_count + end + + # Returns the total amount of open issues for this version. + def open_issues_count + load_issue_counts + @open_issues_count + end + + # Returns the total amount of closed issues for this version. + def closed_issues_count + load_issue_counts + @closed_issues_count + end + + def wiki_page + if project.wiki && !wiki_page_title.blank? + @wiki_page ||= project.wiki.find_page(wiki_page_title) + end + @wiki_page + end + + def to_s; name end + + def to_s_with_project + "#{project} - #{name}" + end + + # Versions are sorted by effective_date and name + # Those with no effective_date are at the end, sorted by name + def <=>(version) + if self.effective_date + if version.effective_date + if self.effective_date == version.effective_date + name == version.name ? id <=> version.id : name <=> version.name + else + self.effective_date <=> version.effective_date + end + else + -1 + end + else + if version.effective_date + 1 + else + name == version.name ? id <=> version.id : name <=> version.name + end + end + end + + def self.fields_for_order_statement(table=nil) + table ||= table_name + ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"] + end + + scope :sorted, lambda { order(fields_for_order_statement) } + + # Returns the sharings that +user+ can set the version to + def allowed_sharings(user = User.current) + VERSION_SHARINGS.select do |s| + if sharing == s + true + else + case s + when 'system' + # Only admin users can set a systemwide sharing + user.admin? + when 'hierarchy', 'tree' + # Only users allowed to manage versions of the root project can + # set sharing to hierarchy or tree + project.nil? || user.allowed_to?(:manage_versions, project.root) + else + true + end + end + end + end + + private + + def load_issue_counts + unless @issue_count + @open_issues_count = 0 + @closed_issues_count = 0 + fixed_issues.group(:status).count.each do |status, count| + if status.is_closed? + @closed_issues_count += count + else + @open_issues_count += count + end + end + @issue_count = @open_issues_count + @closed_issues_count + end + end + + # Update the issue's fixed versions. Used if a version's sharing changes. + def update_issues_from_sharing_change + if sharing_changed? + if VERSION_SHARINGS.index(sharing_was).nil? || + VERSION_SHARINGS.index(sharing).nil? || + VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing) + Issue.update_versions_from_sharing_change self + end + end + end + + # Returns the average estimated time of assigned issues + # or 1 if no issue has an estimated time + # Used to weigth unestimated issues in progress calculation + def estimated_average + if @estimated_average.nil? + average = fixed_issues.average(:estimated_hours).to_f + if average == 0 + average = 1 + end + @estimated_average = average + end + @estimated_average + end + + # Returns the total progress of open or closed issues. The returned percentage takes into account + # the amount of estimated time set for this version. + # + # Examples: + # issues_progress(true) => returns the progress percentage for open issues. + # issues_progress(false) => returns the progress percentage for closed issues. + def issues_progress(open) + @issues_progress ||= {} + @issues_progress[open] ||= begin + progress = 0 + if issues_count > 0 + ratio = open ? 'done_ratio' : 100 + + done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f + progress = done / (estimated_average * issues_count) + end + progress + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/111c3259932e5ffb4a858defd183b9c4499d1ac5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/111c3259932e5ffb4a858defd183b9c4499d1ac5.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1162 @@ +# Lithuanian translations for Ruby on Rails +# by Laurynas Butkus (laurynas.butkus@gmail.com) +# Redmine translation by Gediminas Muižis gediminas.muizis@gmail.com +# and Sergej Jegorov sergej.jegorov@gmail.com +# and Gytis Gurklys gytis.gurklys@gmail.com +# and Andrius KriuÄkovas andrius.kriuckovas@gmail.com + +lt: + direction: ltr + 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: [sekmadienis, pirmadienis, antradienis, treÄiadienis, ketvirtadienis, penktadienis, Å¡eÅ¡tadienis] +# standalone_day_names: [Sekmadienis, Pirmadienis, Antradienis, TreÄiadienis, Ketvirtadienis, Penktadienis, Å eÅ¡tadienis] + abbr_day_names: [Sek, Pir, Ant, Tre, Ket, Pen, Å eÅ¡] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, sausio, vasario, kovo, balandžio, gegužės, birželio, liepos, rugpjÅ«Äio, rugsÄ—jo, spalio, lapkriÄio, gruodžio] + abbr_month_names: [~, Sau, Vas, Kov, Bal, Geg, Bir, Lie, Rgp, Rgs, Spa, Lap, Grd] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "ryto" + pm: "vakaro" + + datetime: + distance_in_words: + half_a_minute: "pusÄ— minutÄ—s" + less_than_x_seconds: + one: "mažiau nei %{count} sekundÄ™" + few: "mažiau nei %{count} sekundes" + many: "mažiau nei %{count} sekundžių" + other: "mažiau nei %{count} sekundžių" + x_seconds: + one: "%{count} sekundÄ—" + few: "%{count} sekundÄ—s" + many: "%{count} sekundžių" + other: "%{count} sekundžių" + less_than_x_minutes: + one: "mažiau nei minutÄ™" + other: "mažiau nei %{count} minutes(Äių)" + x_minutes: + one: "1 minutÄ™" + other: "%{count} minutes(Äių)" + about_x_hours: + one: "apie 1 valandÄ…" + other: "apie %{count} valandas(ų)" + x_hours: + one: "1 valandÄ…" + other: "%{count} valandas(ų)" + x_days: + one: "1 dienÄ…" + other: "%{count} dienas(ų)" + about_x_months: + one: "apie 1 mÄ—nuo" + other: "apie %{count} mÄ—n." + x_months: + one: "1 mÄ—nuo" + other: "%{count} mÄ—n." + about_x_years: + one: "apie 1 metus" + other: "apie %{count} metų" + over_x_years: + one: "virÅ¡ 1 metų" + other: "virÅ¡ %{count} metų" + almost_x_years: + one: "beveik 1 metus" + other: "beveik %{count} metai(us)" + prompts: + year: "Metai" + month: "MÄ—nuo" + day: "Diena" + hour: "Valanda" + minute: "MinutÄ—" + second: "SekundÄ—s" + + number: + format: + separator: "," + delimiter: " " + precision: 3 + + currency: + format: + format: "%n %u" + unit: "Lt" + separator: "," + delimiter: " " + precision: 2 + + percentage: + format: + delimiter: "" + + precision: + format: + delimiter: "" + + human: + format: + delimiter: "" + precision: 3 + storage_units: + # Storage units output formatting. + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u" + units: + byte: + one: "baitas" + few: "baitų(ai)" + many: "baitų(ai)" + other: "baitų(ai)" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + # Rails 2.2 + sentence_connector: "ir" + skip_last_comma: true + # Rails 2.3 + words_connector: ", " + two_words_connector: " ir " + last_word_connector: " ir " + + activerecord: + errors: + template: + header: + one: "IÅ¡saugant objektÄ… %{model} rasta %{count} klaida" + few: "IÅ¡saugant objektÄ… %{model} rasta %{count} klaidų" + many: "IÅ¡saugant objektÄ… %{model} rastos %{count} klaidos" + other: "IÅ¡saugant objektÄ… %{model} rastos %{count} klaidos" + body: "Å iuose laukuose yra klaidų:" + + messages: + inclusion: "nenumatyta reikÅ¡mÄ—" + exclusion: "užimtas" + invalid: "neteisingas" + confirmation: "neteisingai pakartotas" + accepted: "turi bÅ«ti patvirtintas" + empty: "negali bÅ«ti tuÅ¡Äias" + blank: "negali bÅ«ti tuÅ¡Äias" + too_long: + one: "per ilgas (daugiausiai %{count} simbolius)" + few: "per ilgas (daugiausiai %{count} simboliu)" + many: "per ilgas (daugiausiai %{count} simboliu)" + other: "per ilgas (daugiausiai %{count} simboliai)" + too_short: + one: "per trumpas (mažiausiai %{count} simbolius)" + few: "per trumpas (mažiausiai %{count} simboliu)" + many: "per trumpas (mažiausiai %{count} simboliu)" + other: "per trumpas (mažiausiai %{count} simboliai)" + wrong_length: + one: "neteisingo ilgio (turi bÅ«ti lygiai %{count} simbolius)" + few: "neteisingo ilgio (turi bÅ«ti lygiai %{count} simboliu)" + many: "neteisingo ilgio (turi bÅ«ti lygiai %{count} simboliu)" + other: "neteisingo ilgio (turi bÅ«ti lygiai %{count} simboliai)" + taken: "jau užimtas" + not_a_number: "ne skaiÄius" + not_a_date: "is not a valid date" + greater_than: "turi bÅ«ti didesnis už %{count}" + greater_than_or_equal_to: "turi bÅ«ti didesnis arba lygus %{count}" + equal_to: "turi bÅ«ti lygus %{count}" + less_than: "turi bÅ«ti mažesnis už %{count}" + less_than_or_equal_to: "turi bÅ«ti mažesnis arba lygus %{count}" + odd: "turi bÅ«ti nelyginis" + even: "turi bÅ«ti lyginis" + greater_than_start_date: "turi bÅ«ti didesnÄ— negu pradžios data" + not_same_project: "nepriklauso tam paÄiam projektui" + circular_dependency: "Å is ryÅ¡ys sukurtų ciklinÄ™ priklausomybÄ™" + cant_link_an_issue_with_a_descendant: "Darbas negali bÅ«ti susietas su viena iÅ¡ savo darbo dalių" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: PraÅ¡om parinkti + + general_text_No: 'Ne' + general_text_Yes: 'Taip' + general_text_no: 'ne' + general_text_yes: 'taip' + general_lang_name: 'Lithuanian (lietuvių)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Paskyra buvo sÄ—kmingai atnaujinta. + notice_account_invalid_creditentials: Negaliojantis vartotojo vardas ar slaptažodis + notice_account_password_updated: Slaptažodis buvo sÄ—kmingai atnaujintas. + notice_account_wrong_password: Neteisingas slaptažodis + notice_account_register_done: Paskyra buvo sÄ—kmingai sukurta. Kad aktyvintumÄ—te savo paskyrÄ…, paspauskite nuorodÄ…, kuri jums buvo siųsta elektroniniu paÅ¡tu. + notice_account_unknown_email: Nežinomas vartotojas. + notice_can_t_change_password: Å is praneÅ¡imas naudoja iÅ¡orinį autentiÅ¡kumo nustatymo Å¡altinį. Neįmanoma pakeisti slaptažodį. + notice_account_lost_email_sent: Ä® JÅ«sų paÅ¡tÄ… iÅ¡siųstas laiÅ¡kas su naujo slaptažodžio pasirinkimo instrukcija. + notice_account_activated: JÅ«sų paskyra aktyvuota. Galite prisijungti. + notice_successful_create: SÄ—kmingas sukÅ«rimas. + notice_successful_update: SÄ—kmingas atnaujinimas. + notice_successful_delete: SÄ—kmingas panaikinimas. + notice_successful_connection: SÄ—kmingas susijungimas. + notice_file_not_found: Puslapis, į kurį ketinate įeiti, neegzistuoja arba yra paÅ¡alintas. + notice_locking_conflict: Duomenys atnaujinti kito vartotojo. + notice_not_authorized: JÅ«s neturite teisių gauti prieigÄ… prie Å¡io puslapio. + notice_not_authorized_archived_project: Projektas, kurį bandote atidaryti, buvo suarchyvuotas. + notice_email_sent: "LaiÅ¡kas iÅ¡siųstas %{value}" + notice_email_error: "LaiÅ¡ko siuntimo metu įvyko klaida (%{value})" + notice_feeds_access_key_reseted: JÅ«sų Atom raktas buvo atnaujintas. + notice_api_access_key_reseted: JÅ«sų API prieigos raktas buvo atnaujintas. + notice_failed_to_save_issues: "Nepavyko iÅ¡saugoti %{count} problemos(ų) iÅ¡ %{total} pasirinkto: %{ids}." + notice_failed_to_save_members: "Nepavyko iÅ¡saugoti nario(ių): %{errors}." + notice_no_issue_selected: "Nepasirinkta nÄ— viena problema! PraÅ¡om pažymÄ—ti problemÄ…, kuriÄ… norite redaguoti." + notice_account_pending: "JÅ«sų paskyra buvo sukurta ir dabar laukiama administratoriaus patvirtinimo." + notice_default_data_loaded: Numatytoji konfigÅ«racija sÄ—kmingai įkrauta. + notice_unable_delete_version: Neįmanoma panaikinti versijÄ…. + notice_unable_delete_time_entry: Neįmano iÅ¡trinti laiko žurnalo įrašą. + notice_issue_done_ratios_updated: Problemos baigtumo rodikliai atnaujinti. + notice_gantt_chart_truncated: Grafikas buvo sutrumpintas, kadangi jis virÅ¡ija maksimalų (%{max}) leistinų atvaizduoti elementų kiekį + notice_issue_successful_create: Darbas %{id} sukurtas. + + error_can_t_load_default_data: "Numatytoji konfigÅ«racija negali bÅ«ti užkrauta: %{value}" + error_scm_not_found: "Duomenys ir/ar pakeitimai saugykloje (repozitorijoje) neegzistuoja." + error_scm_command_failed: "Ä®vyko klaida jungiantis prie saugyklos: %{value}" + error_scm_annotate: "Ä®raÅ¡as neegzistuoja arba negalima jo atvaizduoti." + error_scm_annotate_big_text_file: "Ä®raÅ¡o negalima atvaizduoti, nes jis virÅ¡ija maksimalų tekstinio failo dydį." + error_issue_not_found_in_project: 'Darbas nerastas arba nesuriÅ¡tas su Å¡iuo projektu' + error_no_tracker_in_project: 'Joks pÄ—dsekys nesusietas su Å¡iuo projektu. PraÅ¡om patikrinti Projekto nustatymus.' + error_no_default_issue_status: Nenustatyta numatytoji darbų bÅ«sena. PraÅ¡ome patikrinti konfigÅ«ravimÄ… ("Administravimas -> Darbų bÅ«senos"). + error_can_not_delete_custom_field: Negalima iÅ¡trinti kliento lauko + error_can_not_delete_tracker: "Å is pÄ—dsekys turi įraÅ¡us ir todÄ—l negali bÅ«ti iÅ¡trintas." + error_can_not_remove_role: "Å i rolÄ— yra naudojama ir negali bÅ«ti iÅ¡trinta." + error_can_not_reopen_issue_on_closed_version: Uždarytai versijai priskirtas darbas negali bÅ«ti atnaujintas. + error_can_not_archive_project: Å io projekto negalima suarchyvuoti + error_issue_done_ratios_not_updated: "Ä®raÅ¡o baigtumo rodikliai nebuvo atnaujinti. " + error_workflow_copy_source: 'PraÅ¡ome pasirinkti pirminį Å¡altinio seklį arba rolÄ™' + error_workflow_copy_target: 'PraÅ¡ome pasirinkti galutinį paskirties seklį(-ius) arba rolÄ™(-s)' + error_unable_delete_issue_status: 'Negalima iÅ¡trinti darbo statuso' + error_unable_to_connect: Negalima prisijungti (%{value}) + error_attachment_too_big: "Å i byla negali bÅ«ti įkelta, nes virÅ¡ija maksimaliÄ… (%{max_size}) leistinÄ… bylos apimtį" + warning_attachments_not_saved: "%{count} byla(-ų) negali bÅ«ti iÅ¡saugota." + + mail_subject_lost_password: "JÅ«sų %{value} slaptažodis" + mail_body_lost_password: 'NorÄ—dami pakeisti slaptažodį, spauskite nuorodÄ…:' + mail_subject_register: "JÅ«sų %{value} paskyros aktyvavimas" + mail_body_register: 'NorÄ—dami aktyvuoti paskyrÄ…, spauskite nuorodÄ…:' + mail_body_account_information_external: "JÅ«s galite naudoti JÅ«sų %{value} paskyrÄ…, norÄ—dami prisijungti." + mail_body_account_information: Informacija apie JÅ«sų paskyrÄ… + mail_subject_account_activation_request: "%{value} paskyros aktyvavimo praÅ¡ymas" + mail_body_account_activation_request: "Užsiregistravo naujas vartotojas (%{value}). Jo paskyra laukia jÅ«sų patvirtinimo:" + mail_subject_reminder: "%{count} jums priskirti darbai per artimiausias %{days} dienų(as)" + mail_body_reminder: "%{count} darbas(ai), kurie yra jums priskirti, baigiasi per artimiausias %{days} dienų(as):" + mail_subject_wiki_content_added: "'%{id}' pridÄ—tas wiki puslapis" + mail_body_wiki_content_added: "'%{id}' wiki puslapį pridÄ—jo %{author}." + mail_subject_wiki_content_updated: "'%{id}' atnaujintas wiki puslapis" + mail_body_wiki_content_updated: "'%{id}' wiki puslapį atnaujino %{author}." + + + field_name: Pavadinimas + field_description: ApraÅ¡as + field_summary: Santrauka + field_is_required: Reikalaujama + field_firstname: Vardas + field_lastname: PavardÄ— + field_mail: El. paÅ¡tas + field_filename: Failas + field_filesize: Dydis + field_downloads: Atsiuntimai + field_author: Autorius + field_created_on: Sukurta + field_updated_on: Atnaujintas(a) + field_field_format: Formatas + field_is_for_all: Visiems projektams + field_possible_values: Galimos reikÅ¡mÄ—s + field_regexp: Pastovi iÅ¡raiÅ¡ka + field_min_length: Minimalus ilgis + field_max_length: Maksimalus ilgis + field_value: VertÄ— + field_category: Kategorija + field_title: Pavadinimas + field_project: Projektas + field_issue: Darbas + field_status: BÅ«sena + field_notes: Pastabos + field_is_closed: Darbas uždarytas + field_is_default: Numatytoji vertÄ— + field_tracker: PÄ—dsekys + field_subject: Tema + field_due_date: Užbaigimo data + field_assigned_to: Paskirtas + field_priority: Prioritetas + field_fixed_version: TikslinÄ— versija + field_user: Vartotojas + field_principal: Vardas + field_role: Vaidmuo + field_homepage: Pagrindinis puslapis + field_is_public: VieÅ¡as + field_parent: Priklauso projektui + field_is_in_roadmap: Darbai rodomi veiklos grafike + field_login: Registracijos vardas + field_mail_notification: Elektroninio paÅ¡to praneÅ¡imai + field_admin: Administratorius + field_last_login_on: Paskutinis prisijungimas + field_language: Kalba + field_effective_date: Data + field_password: Slaptažodis + field_new_password: Naujas slaptažodis + field_password_confirmation: Patvirtinimas + field_version: Versija + field_type: Tipas + field_host: Pagrindinis kompiuteris + field_port: Prievadas + field_account: Paskyra + field_base_dn: Bazinis skiriamasis vardas (base DN) + field_attr_login: Registracijos vardo požymis (login) + field_attr_firstname: Vardo požymis + field_attr_lastname: PavardÄ—s požymis + field_attr_mail: Elektroninio paÅ¡to požymis + field_onthefly: Automatinis vartotojų registravimas + field_start_date: PradÄ—ti + field_done_ratio: "% atlikta" + field_auth_source: AutentiÅ¡kumo nustatymo bÅ«das + field_hide_mail: SlÄ—pti mano elektroninio paÅ¡to adresÄ… + field_comments: Komentaras + field_url: URL + field_start_page: Pradžios puslapis + field_subproject: Sub-projektas + field_hours: valandos + field_activity: Veikla + field_spent_on: Data + field_identifier: Identifikatorius + field_is_filter: Panaudotas kaip filtras + field_issue_to: SusijÄ™s darbas + field_delay: Užlaikymas + field_assignable: Darbai gali bÅ«ti paskirti Å¡iam vaidmeniui + field_redirect_existing_links: Peradresuokite egzistuojanÄias sÄ…sajas + field_estimated_hours: Numatyta trukmÄ— + field_column_names: Skiltys + field_time_entries: Praleistas laikas + field_time_zone: Laiko juosta + field_searchable: Randamas + field_default_value: Numatytoji vertÄ— + field_comments_sorting: Rodyti komentarus + field_parent_title: AukÅ¡tesnio lygio puslapis + field_editable: Redaguojamas + field_watcher: StebÄ—tojas + field_identity_url: OpenID URL + field_content: Turinys + field_group_by: Sugrupuoti pagal + field_sharing: Dalijimasis + field_parent_issue: PagrindinÄ— užduotis + field_member_of_group: "Priskirtojo grupÄ—" + field_assigned_to_role: "Priskirtojo rolÄ—" + field_text: Teksto laukas + field_visible: Matomas + field_warn_on_leaving_unsaved: "Ä®spÄ—ti mane, kai paliekamas puslapis su neiÅ¡saugotu tekstu" + field_issues_visibility: Darbų matomumas + field_is_private: Privatus + field_commit_logs_encoding: Commit žurnalų koduotÄ— + field_scm_path_encoding: SCM kelio koduotÄ— + field_path_to_repository: Saugyklos kelias + field_root_directory: Å akninis katalogas + field_cvsroot: CVSROOT + field_cvs_module: Modulis + + setting_app_title: Programos pavadinimas + setting_app_subtitle: Programos paantraÅ¡tÄ— + setting_welcome_text: Pasveikinimas + setting_default_language: Numatytoji kalba + setting_login_required: Reikalingas autentiÅ¡kumo nustatymas + setting_self_registration: Savi-registracija + setting_attachment_max_size: Priedo maksimalus dydis + setting_issues_export_limit: Darbų eksportavimo riba + setting_mail_from: IÅ¡leidimo elektroninio paÅ¡to adresas + setting_bcc_recipients: Akli tikslios kopijos gavÄ—jai (bcc) + setting_plain_text_mail: Tik tekstas (be HTML) + setting_host_name: Pagrindinio kompiuterio vardas + setting_text_formatting: Teksto formatavimas + setting_wiki_compression: Wiki istorijos suspaudimas + setting_feeds_limit: Perdavimo turinio maksimali riba + setting_default_projects_public: Nauji projektai vieÅ¡i pagal nutylÄ—jimÄ… + setting_autofetch_changesets: Automatinis pakeitimų siuntimas + setting_sys_api_enabled: Ä®galinti WS sandÄ—lio valdymui + setting_commit_ref_keywords: Nurodymo reikÅ¡miniai žodžiai + setting_commit_fix_keywords: Fiksavimo reikÅ¡miniai žodžiai + setting_autologin: Automatinis prisijungimas + setting_date_format: Datos formatas + setting_time_format: Laiko formatas + setting_cross_project_issue_relations: Leisti tarp-projektinius darbų ryÅ¡ius + setting_issue_list_default_columns: Numatytosios skiltys darbų sÄ…raÅ¡e + setting_repositories_encodings: PridÄ—tų failų ir saugyklų Å¡ifravimas + setting_emails_header: LaiÅ¡ko antraÅ¡tÄ— + setting_emails_footer: LaiÅ¡ko paraÅ¡tÄ— + setting_protocol: Protokolas + setting_per_page_options: Ä®rašų puslapyje nustatymas + setting_user_format: Vartotojo atvaizdavimo formatas + setting_activity_days_default: Atvaizduojamos dienos projekto veikloje + setting_display_subprojects_issues: Pagal nutylÄ—jimÄ… rodyti sub-projektų darbus pagrindiniame projekte + setting_enabled_scm: Ä®galinti SCM + setting_mail_handler_body_delimiters: "Trumpinti laiÅ¡kus po vienos iÅ¡ Å¡ių eiluÄių" + setting_mail_handler_api_enabled: Ä®galinti WS įeinantiems laiÅ¡kams + setting_mail_handler_api_key: API raktas + setting_sequential_project_identifiers: Generuoti nuoseklius projekto identifikatorius + setting_gravatar_enabled: Naudoti Gravatar vartotojo paveiksliukus + setting_gravatar_default: Gravatar paveiksliukas pagal nutylÄ—jimÄ… + setting_diff_max_lines_displayed: Maksimalus rodomas pakeitimų eiluÄių skaiÄius + setting_file_max_size_displayed: Maksimalus testinių failų dydis rodomas vienoje eilutÄ—je + setting_repository_log_display_limit: Maksimalus revizijų skaiÄius rodomas žurnale + setting_openid: Leisti OpenID prisijungimÄ… ir registracijÄ… + setting_password_min_length: Minimalus slaptažodžio ilgis + setting_new_project_user_role_id: Vartotojo vaidmuo, suteikiamas ne administratoriui, kuris sukuria projektÄ… + setting_default_projects_modules: Pagal nutylÄ—jimÄ… naujam projektui priskirti moduliai + setting_issue_done_ratio: DArbo įvykdymo progresÄ… skaiÄiuoti su + setting_issue_done_ratio_issue_field: Naudoti darbo laukÄ… + setting_issue_done_ratio_issue_status: Naudoti darbo statusÄ… + setting_start_of_week: SavaitÄ—s pradžios diena + setting_rest_api_enabled: Ä®jungti REST tinklo servisÄ… + setting_cache_formatted_text: PaslÄ—pti formatuotÄ… tekstÄ… + setting_default_notification_option: Numatytosios praneÅ¡imų nuostatos + setting_commit_logtime_enabled: Ä®jungti laiko registravimÄ… + setting_commit_logtime_activity_id: Laiko įrašų veikla + setting_gantt_items_limit: Maksimalus rodmenų skaiÄius rodomas Gantt'o grafike + setting_issue_group_assignment: Leisti darbo priskirimÄ… grupÄ—ms + setting_default_issue_start_date_to_creation_date: Naudoti dabartinÄ™ datÄ… kaip naujų darbų pradžios datÄ… + + permission_add_project: Sukurti projektÄ… + permission_add_subprojects: Kurti sub-projektus + permission_edit_project: Taisyti projektÄ… + permission_select_project_modules: Parinkti projekto modulius + permission_manage_members: Valdyti narius + permission_manage_project_activities: Valdyti projekto veiklas + permission_manage_versions: Valdyti versijas + permission_manage_categories: Valdyti darbų kategorijas + permission_view_issues: UžduoÄių peržiÅ«ra + permission_add_issues: Sukurti darbus + permission_edit_issues: Redaguoti darbus + permission_manage_issue_relations: Valdyti darbų ryÅ¡ius + permission_set_issues_private: Nustatyti darbÄ… vieÅ¡u ar privaÄiu + permission_set_own_issues_private: Nustatyti savo darbus vieÅ¡ais ar privaÄiais + permission_add_issue_notes: Sukurti pastabas + permission_edit_issue_notes: Redaguoti pastabas + permission_edit_own_issue_notes: Redaguoti savo pastabas + permission_move_issues: Perkelti darbus + permission_delete_issues: PaÅ¡alinti darbus + permission_manage_public_queries: Valdyti vieÅ¡as užklausas + permission_save_queries: IÅ¡saugoti užklausas + permission_view_gantt: Matyti Gantt grafikÄ… + permission_view_calendar: Matyti kalendorių + permission_view_issue_watchers: Matyti stebÄ—tojų sÄ…rašą + permission_add_issue_watchers: PridÄ—ti stebÄ—tojus + permission_delete_issue_watchers: PaÅ¡alinti stebÄ—tojus + permission_log_time: Regsitruoti dirbtÄ… laikÄ… + permission_view_time_entries: Matyti dirbtÄ… laikÄ… + permission_edit_time_entries: Redaguoti laiko įraÅ¡us + permission_edit_own_time_entries: Redaguoti savo laiko įraÅ¡us + permission_manage_news: Valdyti naujienas + permission_comment_news: Komentuoti naujienas + permission_view_documents: Matyti dokumentus + permission_manage_files: Valdyti failus + permission_view_files: Matyti failus + permission_manage_wiki: Valdyti wiki + permission_rename_wiki_pages: Pervadinti wiki puslapius + permission_delete_wiki_pages: PaÅ¡alinti wiki puslapius + permission_view_wiki_pages: Matyti wiki + permission_view_wiki_edits: Matyti wiki istorijÄ… + permission_edit_wiki_pages: Redaguoti wiki puslapius + permission_delete_wiki_pages_attachments: PaÅ¡alinti priedus + permission_protect_wiki_pages: Apsaugoti wiki puslapius + permission_manage_repository: Valdyti saugyklÄ… + permission_browse_repository: PeržiÅ«rÄ—ti saugyklÄ… + permission_view_changesets: Matyti pakeitimus + permission_commit_access: Prieiga prie pakeitimų + permission_manage_boards: Valdyti forumus + permission_view_messages: Matyti praneÅ¡imus + permission_add_messages: Skelbti praneÅ¡imus + permission_edit_messages: Redaguoti praneÅ¡imus + permission_edit_own_messages: Redaguoti savo praneÅ¡imus + permission_delete_messages: PaÅ¡alinti praneÅ¡imus + permission_delete_own_messages: PaÅ¡alinti savo praneÅ¡imus + permission_export_wiki_pages: Eksportuoti wiki puslapius + permission_manage_subtasks: Valdyti darbo dalis + + project_module_issue_tracking: Darbų pÄ—dsekys + project_module_time_tracking: Laiko pÄ—dsekys + project_module_news: Naujienos + project_module_documents: Dokumentai + project_module_files: Failai + project_module_wiki: Wiki + project_module_repository: Saugykla + project_module_boards: Forumai + project_module_calendar: Kalendorius + project_module_gantt: Gantt + + label_user: Vartotojas + label_user_plural: Vartotojai + label_user_new: Naujas vartotojas + label_user_anonymous: Anonimas + label_project: Projektas + label_project_new: Naujas projektas + label_project_plural: Projektai + label_x_projects: + zero: nÄ—ra projektų + one: 1 projektas + other: "%{count} projektų" + label_project_all: Visi Projektai + label_project_latest: Naujausi projektai + label_issue: Darbas + label_issue_new: Naujas darbas + label_issue_plural: Darbai + label_issue_view_all: PeržiÅ«rÄ—ti visus darbus + label_issues_by: "Darbai pagal %{value}" + label_issue_added: Darbas pridÄ—tas + label_issue_updated: Darbas atnaujintas + label_issue_note_added: Pastaba pridÄ—ta + label_issue_status_updated: Statusas atnaujintas + label_issue_priority_updated: Prioritetas atnaujintas + label_document: Dokumentas + label_document_new: Naujas dokumentas + label_document_plural: Dokumentai + label_document_added: Dokumentas pridÄ—tas + label_role: Vaidmuo + label_role_plural: Vaidmenys + label_role_new: Naujas vaidmuo + label_role_and_permissions: Vaidmenys ir leidimai + label_role_anonymous: Anonimas + label_role_non_member: NÄ—ra narys + label_member: Narys + label_member_new: Naujas narys + label_member_plural: Nariai + label_tracker: PÄ—dsekys + label_tracker_plural: PÄ—dsekiai + label_tracker_new: Naujas pÄ—dsekys + label_workflow: Darbų eiga + label_issue_status: Darbo bÅ«sena + label_issue_status_plural: Darbų bÅ«senos + label_issue_status_new: Nauja bÅ«sena + label_issue_category: Darbo kategorija + label_issue_category_plural: Darbo kategorijos + label_issue_category_new: Nauja kategorija + label_custom_field: Kliento laukas + label_custom_field_plural: Kliento laukai + label_custom_field_new: Naujas kliento laukas + label_enumerations: IÅ¡vardinimai + label_enumeration_new: Nauja vertÄ— + label_information: Informacija + label_information_plural: Informacija + label_please_login: PraÅ¡om prisijungti + label_register: Užsiregistruoti + label_login_with_open_id_option: arba prisijunkite su OpenID + label_password_lost: Prarastas slaptažodis + label_home: Pagrindinis + label_my_page: Mano puslapis + label_my_account: Mano paskyra + label_my_projects: Mano projektai + label_my_page_block: Mano puslapio blokas + label_administration: Administravimas + label_login: Prisijungti + label_logout: Atsijungti + label_help: Pagalba + label_reported_issues: PraneÅ¡ti darbai + label_assigned_to_me_issues: Darbai, priskirti man + label_last_login: Paskutinis prisijungimas + label_registered_on: Užregistruota + label_activity: Veikla + label_overall_activity: Visa veikla + label_user_activity: "%{value} veikla" + label_new: Naujas + label_logged_as: PrisijungÄ™s kaip + label_environment: Aplinka + label_authentication: AutentiÅ¡kumo nustatymas + label_auth_source: AutentiÅ¡kumo nustatymo bÅ«das + label_auth_source_new: Naujas autentiÅ¡kumo nustatymo bÅ«das + label_auth_source_plural: AutentiÅ¡kumo nustatymo bÅ«dai + label_subproject_plural: Sub-projektai + label_subproject_new: Naujas sub-projektas + label_and_its_subprojects: "%{value} projektas ir jo sub-projektai" + label_min_max_length: Min - Maks ilgis + label_list: SÄ…raÅ¡as + label_date: Data + label_integer: Sveikasis skaiÄius + label_float: Slankiojo kablelio skaiÄius + label_boolean: Loginis + label_string: Tekstas + label_text: Ilgas tekstas + label_attribute: Požymis + label_attribute_plural: Požymiai + label_no_data: NÄ—ra kÄ… atvaizduoti + label_change_status: Pakeitimo bÅ«sena + label_history: Istorija + label_attachment: Failas + label_attachment_new: Naujas failas + label_attachment_delete: PaÅ¡alinkite failÄ… + label_attachment_plural: Failai + label_file_added: Failas pridÄ—tas + label_report: Ataskaita + label_report_plural: Ataskaitos + label_news: Naujiena + label_news_new: PridÄ—ti naujienas + label_news_plural: Naujienos + label_news_latest: PaskutinÄ—s naujienos + label_news_view_all: PeržiÅ«rÄ—ti visas naujienas + label_news_added: Naujiena pridÄ—ta + label_news_comment_added: Prie naujienos pridÄ—tas komentaras + label_settings: Nustatymai + label_overview: Apžvalga + label_version: Versija + label_version_new: Nauja versija + label_version_plural: Versijos + label_close_versions: Uždaryti užbaigtas versijas + label_confirmation: Patvirtinimas + label_export_to: 'Eksportuoti į:' + label_read: Skaitykite... + label_public_projects: VieÅ¡i projektai + label_open_issues: atidaryta + label_open_issues_plural: atidaryti + label_closed_issues: uždaryta + label_closed_issues_plural: uždaryti + label_x_open_issues_abbr_on_total: + zero: 0 atvirų / %{total} + one: 1 atviras / %{total} + other: "%{count} atviri / %{total}" + label_x_open_issues_abbr: + zero: 0 atvirų + one: 1 atviras + other: "%{count} atviri" + label_x_closed_issues_abbr: + zero: 0 uždarytų + one: 1 uždarytas + other: "%{count} uždarytų" + label_total: IÅ¡ viso + label_permissions: Leidimai + label_current_status: DabartinÄ— bÅ«sena + label_new_statuses_allowed: Naujos bÅ«senos galimos + label_all: visi(os) + label_none: joks + label_nobody: niekas + label_next: Kitas + label_previous: Ankstesnis + label_used_by: Naudotas + label_details: DetalÄ—s + label_add_note: PridÄ—kite pastabÄ… + label_per_page: Puslapyje + label_calendar: Kalendorius + label_months_from: mÄ—nesiai nuo + label_gantt: Gantt + label_internal: Vidinis + label_last_changes: "paskutiniai %{count} pokyÄiai(-ių)" + label_change_view_all: PeržiÅ«rÄ—ti visus pakeitimus + label_personalize_page: Suasmeninti šį puslapį + label_comment: Komentaras + label_comment_plural: Komentarai + label_x_comments: + zero: nÄ—ra komentarų + one: 1 komentaras + other: "%{count} komentarų" + label_comment_add: PridÄ—kite komentarÄ… + label_comment_added: Komentaras pridÄ—tas + label_comment_delete: PaÅ¡alinti komentarus + label_query: Užklausa + label_query_plural: Užklausos + label_query_new: Nauja užklausa + label_my_queries: Mano sukurtos užklausos + label_filter_add: PridÄ—ti filtrÄ… + label_filter_plural: Filtrai + label_equals: yra + label_not_equals: nÄ—ra + label_in_less_than: anksÄiau nei po + label_in_more_than: vÄ—liau nei po + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_between: tarp + label_in: per + label_today: Å¡iandien + label_all_time: visas laikas + label_yesterday: vakar + label_this_week: Å¡iÄ… savaitÄ™ + label_last_week: praeita savaitÄ— + label_last_n_days: "paskutinių %{count} dienų" + label_this_month: Å¡is mÄ—nuo + label_last_month: praeitas mÄ—nuo + label_this_year: Å¡iemet + label_date_range: Dienų diapazonas + label_less_than_ago: vÄ—liau nei prieÅ¡ + label_more_than_ago: anksÄiau nei prieÅ¡ + label_ago: prieÅ¡ + label_contains: turi + label_not_contains: neturi + label_day_plural: dienų(-os) + label_repository: Saugykla + label_repository_plural: Saugyklos + label_browse: NarÅ¡yti + label_branch: Å aka + label_tag: Tag + label_revision: Revizija + label_revision_plural: Revizijos + label_revision_id: Revizija %{value} + label_associated_revisions: Susijusios revizijos + label_added: pridÄ—tas + label_modified: pakeistas + label_copied: nukopijuotas + label_renamed: pervardintas + label_deleted: paÅ¡alintas + label_latest_revision: PaskutinÄ— revizija + label_latest_revision_plural: PaskutinÄ—s revizijos + label_view_revisions: PeržiÅ«rÄ—ti revizijas + label_view_all_revisions: PeržiÅ«rÄ—ti visas revizijas + label_max_size: Maksimalus dydis + label_sort_highest: Perkelti į viršūnÄ™ + label_sort_higher: Perkelti į viršų + label_sort_lower: Perkelti žemyn + label_sort_lowest: Perkelti į apaÄiÄ… + label_roadmap: Veiklos grafikas + label_roadmap_due_in: "Baigiasi po %{value}" + label_roadmap_overdue: "%{value} vÄ—luojama" + label_roadmap_no_issues: Å iai versijai nepriskirtas koks darbas + label_search: IeÅ¡koti + label_result_plural: Rezultatai + label_all_words: Visi žodžiai + label_wiki: Wiki + label_wiki_edit: Wiki redakcija + label_wiki_edit_plural: Wiki redakcijos + label_wiki_page: Wiki puslapis + label_wiki_page_plural: Wiki puslapiai + label_index_by_title: Indeksuoti pagal pavadinimÄ… + label_index_by_date: Indeksuoti pagal datÄ… + label_current_version: Einamoji versija + label_preview: PeržiÅ«ra + label_feed_plural: Kanalai + label_changes_details: Visų pakeitimų detalÄ—s + label_issue_tracking: Darbų sekimas + label_spent_time: Dirbtas laikas + label_overall_spent_time: Visas dirbtas laikas + label_f_hour: "%{value} valanda" + label_f_hour_plural: "%{value} valandų(-os)" + label_time_tracking: Laiko sekimas + label_change_plural: Pakeitimai + label_statistics: Statistika + label_commits_per_month: Ä®kÄ—limai per mÄ—nesį + label_commits_per_author: Ä®kÄ—limai pagal autorių + label_diff: skirt + label_view_diff: Skirtumų peržiÅ«ra + label_diff_inline: įterptas + label_diff_side_by_side: Å¡alia + label_options: Pasirinkimai + label_copy_workflow_from: Kopijuoti darbų eiga iÅ¡ + label_permissions_report: Leidimų praneÅ¡imas + label_watched_issues: Stebimi darbai + label_related_issues: SusijÄ™ darbai + label_applied_status: Taikomoji bÅ«sena + label_loading: Kraunama... + label_relation_new: Naujas ryÅ¡ys + label_relation_delete: PaÅ¡alinti ryšį + label_relates_to: susietas su + label_duplicates: dubliuoja + label_duplicated_by: dubliuojasi su + label_blocks: blokuoja + label_blocked_by: blokuojamas + label_precedes: ankstesnÄ—(-is) + label_follows: seka + label_end_to_start: užbaigti, kad pradÄ—ti + label_end_to_end: užbaigti, kad pabaigti + label_start_to_start: pradÄ—kite pradÄ—ti + label_start_to_end: pradÄ—kite užbaigti + label_stay_logged_in: Likti prisijungus + label_disabled: iÅ¡jungta(-as) + label_show_completed_versions: Rodyti užbaigtas versijas + label_me: aÅ¡ + label_board: Forumas + label_board_new: Naujas forumas + label_board_plural: Forumai + label_board_locked: Užrakinta + label_board_sticky: Lipnus + label_topic_plural: Temos + label_message_plural: PraneÅ¡imai + label_message_last: Paskutinis praneÅ¡imas + label_message_new: Naujas praneÅ¡imas + label_message_posted: PraneÅ¡imas pridÄ—tas + label_reply_plural: Atsakymai + label_send_information: Nusiųsti paskyros informacijÄ… vartotojui + label_year: Metai + label_month: MÄ—nuo + label_week: SavaitÄ— + label_date_from: Nuo + label_date_to: Iki + label_language_based: Pagrįsta vartotojo kalba + label_sort_by: "Rūšiuoti pagal %{value}" + label_send_test_email: Nusiųsti bandomÄ…jį laiÅ¡kÄ… + label_feeds_access_key: Atom prieigos raktas + label_missing_feeds_access_key: TrÅ«ksta Atom prieigos rakto + label_feeds_access_key_created_on: "Atom prieigos raktas sukurtas prieÅ¡ %{value}" + label_module_plural: Moduliai + label_added_time_by: "PridÄ—jo %{author} prieÅ¡ %{age}" + label_updated_time_by: "Atnaujino %{author} prieÅ¡ %{age}" + label_updated_time: "Atnaujinta prieÅ¡ %{value}" + label_jump_to_a_project: Å uolis į projektÄ…... + label_file_plural: Failai + label_changeset_plural: Pakeitimų rinkiniai + label_default_columns: Numatytieji stulpeliai + label_no_change_option: (Jokio pakeitimo) + label_bulk_edit_selected_issues: MasiÅ¡kai redaguoti pasirinktus darbus + label_bulk_edit_selected_time_entries: MasiÅ¡kai redaguotumÄ—te pasirinktus laiko įraÅ¡us + label_theme: Tema + label_default: Numatyta(-as) + label_search_titles_only: IeÅ¡koti tiktai pavadinimų + label_user_mail_option_all: "Bet kokiam įvykiui visuose mano projektuose" + label_user_mail_option_selected: "Bet kokiam įvykiui tiktai pasirinktuose projektuose ..." + label_user_mail_option_none: "NÄ—ra įvykių" + label_user_mail_option_only_my_events: "Tiktai įvykiai, kuriuos stebiu arba esu įtrauktas" + label_user_mail_option_only_assigned: "Tiktai įvykiai, kuriems esu priskirtas" + label_user_mail_option_only_owner: "Tiktai įvykiai, kurių Å¡eimininkas esu aÅ¡" + label_user_mail_no_self_notified: "Nenoriu bÅ«ti informuotas apie pakeitimus, kuriuos pats atlieku" + label_registration_activation_by_email: paskyros aktyvacija per e-paÅ¡tÄ… + label_registration_manual_activation: rankinÄ— paskyros aktyvacija + label_registration_automatic_activation: automatinÄ— paskyros aktyvacija + label_display_per_page: "%{value} įrašų puslapyje" + label_age: Amžius + label_change_properties: Pakeisti nustatymus + label_general: Bendri(-as) + label_more: Daugiau + label_scm: SCM + label_plugins: Ä®skiepiai + label_ldap_authentication: LDAP autentifikacija + label_downloads_abbr: siunt. + label_optional_description: ApibÅ«dinimas (laisvai pasirenkamas) + label_add_another_file: PridÄ—ti kitÄ… failÄ… + label_preferences: SavybÄ—s + label_chronological_order: Chronologine tvarka + label_reverse_chronological_order: Atbuline chronologine tvarka + label_planning: Planavimas + label_incoming_emails: Ä®einantys laiÅ¡kai + label_generate_key: Generuoti raktÄ… + label_issue_watchers: StebÄ—tojai + label_example: Pavyzdys + label_display: Demonstruoti + label_sort: Rūšiuoti + label_ascending: DidÄ—jantis + label_descending: Mažėjantis + label_date_from_to: Nuo %{start} iki %{end} + label_wiki_content_added: Wiki puslapis pridÄ—tas + label_wiki_content_updated: Wiki puslapis atnaujintas + label_group: GrupÄ— + label_group_plural: GrupÄ—s + label_group_new: Nauja grupÄ— + label_time_entry_plural: Sprendimo laikas + label_version_sharing_none: Nesidalinama + label_version_sharing_descendants: Su sub-projektais + label_version_sharing_hierarchy: Su projekto hierarchija + label_version_sharing_tree: Su projekto medžiu + label_version_sharing_system: Su visais projektais + label_update_issue_done_ratios: Atnaujinti darbo atlikimo progresÄ… + label_copy_source: Å altinis + label_copy_target: Tikslas + label_copy_same_as_target: Toks pat kaip tikslas + label_display_used_statuses_only: Rodyti tik tuos statusus, kurie naudojami Å¡io pÄ—dsekio + label_api_access_key: API prieigos raktas + label_missing_api_access_key: TrÅ«ksta API prieigos rakto + label_api_access_key_created_on: "API prieigos raktas sukurtas prieÅ¡ %{value}" + label_profile: Profilis + label_subtask_plural: Darbo dalys + label_project_copy_notifications: Siųsti paÅ¡to praneÅ¡imus kopijuojant projektÄ… + label_principal_search: "IeÅ¡koti vartotojo arba grupÄ—s:" + label_user_search: "IeÅ¡koti vartotojo:" + label_issues_visibility_all: Visi darbai + label_issues_visibility_public: Visi vieÅ¡i darbai + label_issues_visibility_own: Darbai, sukurti vartotojo arba jam priskirti + label_git_report_last_commit: Nurodyti paskutinį failų ir katalogų pakeitimÄ… + label_parent_revision: PirminÄ— revizija + label_child_revision: Sekanti revizija + label_export_options: "%{export_format} eksportavimo nustatymai" + + button_login: Registruotis + button_submit: Pateikti + button_save: IÅ¡saugoti + button_check_all: ŽymÄ—ti visus + button_uncheck_all: AtžymÄ—ti visus + button_collapse_all: Sutraukti visus + button_expand_all: IÅ¡skleisti visus + button_delete: PaÅ¡alinti + button_create: Sukurti + button_create_and_continue: Sukurti ir tÄ™sti + button_test: Testas + button_edit: Redaguoti + button_edit_associated_wikipage: "Redaguoti susijusį Wiki puslapį: %{page_title}" + button_add: PridÄ—ti + button_change: Keisti + button_apply: Pritaikyti + button_clear: IÅ¡valyti + button_lock: Rakinti + button_unlock: Atrakinti + button_download: Atsisiųsti + button_list: SÄ…raÅ¡as + button_view: ŽiÅ«rÄ—ti + button_move: Perkelti + button_move_and_follow: Perkelti ir sekti + button_back: Atgal + button_cancel: AtÅ¡aukti + button_activate: Aktyvinti + button_sort: Rūšiuoti + button_log_time: Registruoti laikÄ… + button_rollback: Grįžti į Å¡iÄ… versijÄ… + button_watch: StebÄ—ti + button_unwatch: NestebÄ—ti + button_reply: Atsakyti + button_archive: Archyvuoti + button_unarchive: IÅ¡pakuoti + button_reset: Atstatyti + button_rename: Pervadinti + button_change_password: Pakeisti slaptažodį + button_copy: Kopijuoti + button_copy_and_follow: Kopijuoti ir laikytis + button_annotate: RaÅ¡yti pastabÄ… + button_update: Atnaujinti + button_configure: KonfigÅ«ruoti + button_quote: Cituoti + button_duplicate: Dubliuoti + button_show: Rodyti + button_edit_section: Redaguoti šį skirsnį + button_export: Eksportuoti + + status_active: aktyvus + status_registered: užregistruotas + status_locked: užrakintas + + version_status_open: atidaryta + version_status_locked: užrakinta + version_status_closed: uždaryta + + field_active: Aktyvus + + text_select_mail_notifications: IÅ¡rinkite veiksmus, apie kuriuos bÅ«tų praneÅ¡ta elektroniniu paÅ¡tu. + text_regexp_info: pvz. ^[A-Z0-9]+$ + text_min_max_length_info: 0 reiÅ¡kia jokių apribojimų + text_project_destroy_confirmation: Ar esate įsitikinÄ™s, kad norite paÅ¡alinti šį projektÄ… ir visus susijusius duomenis? + text_subprojects_destroy_warning: "Å is(-ie) sub-projektas(-ai): %{value} taip pat bus iÅ¡trintas(-i)." + text_workflow_edit: IÅ¡rinkite vaidmenį ir pÄ—dsekį, kad redaguotumÄ—te darbų eigÄ… + text_are_you_sure: Ar esate įsitikinÄ™s? + text_journal_changed: "%{label} pakeistas(a) iÅ¡ %{old} į %{new}" + text_journal_changed_no_detail: "%{label} atnaujintas(a)" + text_journal_set_to: "%{label} nustatytas(a) į %{value}" + text_journal_deleted: "%{label} iÅ¡trintas(a) (%{old})" + text_journal_added: "%{label} pridÄ—tas(a) %{value}" + text_tip_issue_begin_day: užduotis, prasidedanti Å¡iÄ… dienÄ… + text_tip_issue_end_day: užduotis, pasibaigianti Å¡iÄ… dienÄ… + text_tip_issue_begin_end_day: užduotis, prasidedanti ir pasibaigianti Å¡iÄ… dienÄ… + text_project_identifier_info: 'Mažosios raidÄ—s (a-z), skaiÄiai ir brÅ«kÅ¡niai galimi.
    IÅ¡saugojus, identifikatorius negali bÅ«ti keiÄiamas.' + text_caracters_maximum: "%{count} simbolių maksimumas." + text_caracters_minimum: "Turi bÅ«ti mažiausiai %{count} simbolių ilgio." + text_length_between: "Ilgis tarp %{min} ir %{max} simbolių." + text_tracker_no_workflow: Jokia darbų eiga neapibrėžta Å¡iam pÄ—dsekiui + text_unallowed_characters: Neleistini simboliai + text_comma_separated: Leistinos kelios reikÅ¡mÄ—s (atskirtos kableliu). + text_line_separated: Galimos kelios reikÅ¡mÄ—s (viena linija vienai vertei). + text_issues_ref_in_commit_messages: Darbų susiejimas ir fiksavimas pavedimų žinutÄ—se + text_issue_added: "Darbas %{id} buvo praneÅ¡tas (by %{author})." + text_issue_updated: "Darbas %{id} buvo atnaujintas (by %{author})." + text_wiki_destroy_confirmation: Ar esate įsitikinÄ™s, kad norite paÅ¡alinti šį wiki puslapį ir visÄ… jo turinį? + text_issue_category_destroy_question: "Kai kurie darbai (%{count}) yra paskirti Å¡iai kategorijai. KÄ… jÅ«s norite daryti?" + text_issue_category_destroy_assignments: PaÅ¡alinti kategorijos užduotis + text_issue_category_reassign_to: IÅ¡ naujo priskirti darbus Å¡iai kategorijai + text_user_mail_option: "NeiÅ¡rinktiems projektams, jÅ«s gausite tiktai praneÅ¡imus apie tuos įvykius, kuriuos jÅ«s stebite, arba į kuriuos esate įtrauktas (pvz. darbai, kurių autorius jÅ«s esate ar esate priskirtas)." + text_no_configuration_data: "Vaidmenys, pÄ—dsekiai, darbų bÅ«senos ir darbų eiga dar nebuvo konfigÅ«ruoti.\nGriežtai rekomenduojam užkrauti numatytÄ…jÄ… (default) konfigÅ«racijÄ…. Užkrovus, galÄ—site jÄ… modifikuoti." + text_load_default_configuration: Užkrauti numatytÄ…jÄ… konfigÅ«racijÄ… + text_status_changed_by_changeset: "Pakeista %{value} revizijoje." + text_time_logged_by_changeset: "Applied in changeset %{value}." + text_issues_destroy_confirmation: 'Ar jÅ«s tikrai norite sunaikinti pažymÄ—tÄ…(-us) darbÄ…(-us)?' + text_issues_destroy_descendants_confirmation: Taip pat bus iÅ¡trinta(-os) %{count} darbo dalis(ys). + text_time_entries_destroy_confirmation: 'Ar jÅ«s tikrai norite iÅ¡trinti pasirinktÄ…(-us) laiko įrašą(-us)?' + text_select_project_modules: 'Parinkite modulius, kuriuos norite naudoti Å¡iame projekte:' + text_default_administrator_account_changed: Administratoriaus numatytoji paskyra pakeista + text_file_repository_writable: Ä® failų saugyklÄ… saugoti galima (RW) + text_plugin_assets_writable: Ä® įskiepių apraÅ¡o punktų katalogÄ… įraÅ¡yti galima + text_rmagick_available: RMagick pasiekiamas (pasirinktinai) + text_destroy_time_entries_question: "Naikinamam darbui priskirta %{hours} valandų. KÄ… norite su jomis daryti?" + text_destroy_time_entries: IÅ¡trinti įraÅ¡ytas valandas + text_assign_time_entries_to_project: Priskirti įraÅ¡ytas valandas prie projekto + text_reassign_time_entries: 'Priskirti įraÅ¡ytas valandas Å¡iam darbui:' + text_user_wrote: "%{value} parašė:" + text_enumeration_destroy_question: "%{count} objektai(ų) priskirti Å¡iai reikÅ¡mei." + text_enumeration_category_reassign_to: 'Priskirti juos Å¡iai reikÅ¡mei:' + text_email_delivery_not_configured: "El.paÅ¡to siuntimas nesukonfigÅ«ruotas, ir perspÄ—jimai neaktyvus.\nSukonfigÅ«ruokite savo SMTP serverį byloje config/configuration.yml ir perleiskite programÄ… norÄ—dami pritaikyti pakeitimus." + text_repository_usernames_mapping: "Parinkite ar atnaujinkite Redmine vartotojÄ…, kuris paminÄ—tas saugyklos žurnale.\nVartotojai, turintys tÄ… patį Redmine ir saugyklos vardÄ… ar el. paÅ¡tÄ… yra automatiÅ¡kai suriÅ¡ti." + text_diff_truncated: "... Å is diff'as nukarpytas, nes jis virÅ¡ijo maksimalų rodomų eiluÄių skaiÄių." + text_custom_field_possible_values_info: 'Po vienÄ… eilutÄ™ kiekvienai reikÅ¡mei' + text_wiki_page_destroy_question: "Å is puslapis turi %{descendants} susijusių arba iÅ¡vestinių puslapių. KÄ… norÄ—tumÄ—te daryti?" + text_wiki_page_nullify_children: Laikyti susijusius puslapius kaip pagrindinius puslapius + text_wiki_page_destroy_children: "PaÅ¡alinti susijusius puslapius ir jų palikuonis" + text_wiki_page_reassign_children: "Priskirkite iÅ¡ naujo 'susijusius' puslapius Å¡iam pagrindiniam puslapiui" + text_own_membership_delete_confirmation: "JÅ«s esate pasiruošęs panaikinti dalį arba visus leidimus ir po Å¡io pakeitimo galite prarasti Å¡io projekto redagavimo galimybÄ™.\nAr jÅ«s esate įsitikinÄ™s ir tÄ™sti?" + text_zoom_in: Priartinti + text_zoom_out: Nutolinti + text_warn_on_leaving_unsaved: "Dabartinis puslapis turi neiÅ¡saugoto teksto, kuris bus prarastas, jeigu paliksite šį puslapį." + text_scm_path_encoding_note: "Numatytasis: UTF-8" + text_git_repository_note: Saugykla (repository) yra plika ir vietinÄ— (pvz. /gitrepo, c:\gitrepo) + text_mercurial_repository_note: VietinÄ— saugykla (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Komanda + text_scm_command_version: Versija + + default_role_manager: Vadovas + default_role_developer: Projektuotojas + default_role_reporter: Pranešėjas + default_tracker_bug: Klaida + default_tracker_feature: YpatybÄ— + default_tracker_support: Palaikymas + default_issue_status_new: Naujas + default_issue_status_in_progress: Vykdomas + default_issue_status_resolved: IÅ¡sprÄ™stas + default_issue_status_feedback: Grįžtamasis ryÅ¡ys + default_issue_status_closed: Uždarytas + default_issue_status_rejected: Atmestas + default_doc_category_user: Vartotojo dokumentacija + default_doc_category_tech: TechninÄ— dokumentacija + default_priority_low: Žemas + default_priority_normal: Normalus + default_priority_high: AukÅ¡tas + default_priority_urgent: Skubus + default_priority_immediate: NeatidÄ—liotinas + default_activity_design: Projektavimas + default_activity_development: Vystymas + + enumeration_issue_priorities: Darbo prioritetai + enumeration_doc_categories: Dokumento kategorijos + enumeration_activities: Veiklos (laiko) sekimas + enumeration_system_activity: Sistemos veikla + + description_filter: Filtras + description_search: PaieÅ¡kos laukas + description_choose_project: Projektai + description_project_scope: PaieÅ¡kos sritis + description_notes: Pastabos + description_message_content: ŽinutÄ—s turinys + description_query_sort_criteria_attribute: Rūšiuoti atributÄ… + description_query_sort_criteria_direction: Rūšiuoti kryptį + description_user_mail_notification: PaÅ¡to praneÅ¡imų nustatymai + description_available_columns: Galimi Stulpeliai + description_selected_columns: Pasirinkti Stulpeliai + description_all_columns: Visi stulpeliai + description_issue_category_reassign: Pasirinkti darbo kategorijÄ… + description_wiki_subpages_reassign: Pasirinkti naujÄ… pagrindinį puslapį + description_date_range_list: PasirinkitÄ™ diapazonÄ… iÅ¡ sÄ…raÅ¡o + description_date_range_interval: Pasirinkite diapazonÄ… pasirinkdami pradžios ir pabaigos datas + description_date_from: Ä®vesti pradžios datÄ… + description_date_to: Ä®vesti pabaigos datÄ… + + label_additional_workflow_transitions_for_assignee: Papildomi darbų eigos variantai kai darbas paskirtas vartotojui + label_additional_workflow_transitions_for_author: Papildomi darbų eigos variantai kai vartotojas yra darbo autorius + notice_failed_to_save_time_entries: "Nepavyko iÅ¡saugoti %{count} laiko žurnalo įrašų iÅ¡ %{total} parinktų: %{ids}." + label_x_issues: + zero: 0 darbas + one: 1 darbas + other: "%{count} darbai(ų)" + label_repository_new: Nauja saugykla + field_repository_is_default: PagrindinÄ— saugykla + label_copy_attachments: Kopijuoti priedus + label_item_position: "%{position}/%{count}" + label_completed_versions: Užbaigtos versijos + text_project_identifier_info: Leidžiamos tik mažosios raidÄ—s (a-z), skaitmenys, brÅ«kÅ¡neliai ir pabraukimo simboliai.
    KartÄ… iÅ¡saugojus pakeitimai negalimi + field_multiple: Keletas reikÅ¡mių + setting_commit_cross_project_ref: Leisti visų kitų projektų įraÅ¡us susieti nuorodomis ir sutaisyti + text_issue_conflict_resolution_add_notes: IÅ¡saugoti mano žinutÄ™ ir atmesti likusius mano pataisymus + text_issue_conflict_resolution_overwrite: IÅ¡saugoti mano pakeitimus (ankstesnių pakeitimų žinutÄ—s bus iÅ¡saugotos, taÄiau kai kurie pakeitimai bus perraÅ¡yti) + notice_issue_update_conflict: Darbas buvo pakoreguotas kito vartotojo kol jÅ«s atlikote pakeitimus. + text_issue_conflict_resolution_cancel: Atmesti visus mano pakeitimus ir iÅ¡ naujo rodyti %{link} + permission_manage_related_issues: Tvarkyti susietus darbus + field_auth_source_ldap_filter: LDAP filtras + label_search_for_watchers: IeÅ¡koti vartotojų kuriuos įtraukti kaip stebÄ—tojus + notice_account_deleted: JÅ«sų paskyra panaikinta. + setting_unsubscribe: Leisti vartotojams panaikinti savo paskyrÄ… + button_delete_my_account: Panaikinti savo paskyrÄ… + text_account_destroy_confirmation: |- + Ar tikrai norite tÄ™sti? + JÅ«sų paskyra bus panaikinta ir nebus galimybÄ—s jos atkurti. + error_session_expired: JÅ«sų sesija pasibaigÄ—. PraÅ¡ome prisijunti iÅ¡ naujo. + text_session_expiration_settings: "Ä®spÄ—jimas: atlikus Å¡iuos pakeitimus visos aktyvios sesijos gali nustoti galiojusios (įskaitant jÅ«sų sesijÄ…)." + setting_session_lifetime: Sesijos maksimalus galiojimas + setting_session_timeout: Sesijos neveiklumo laiko tarpas + label_session_expiration: BaigÄ—si sujungimo sesija + permission_close_project: Uždaryti / atnaujinti projektÄ… + label_show_closed_projects: Matyti uždarytus projektus + button_close: Uždaryti + button_reopen: Atnaujinti + project_status_active: aktyvus + project_status_closed: uždarytas + project_status_archived: archyvuotas + text_project_closed: Å is projektas yra uždarytas, prieinamas tik peržiÅ«rai. + notice_user_successful_create: Vartotojas %{id} sukurtas. + field_core_fields: Standartiniai laukai + field_timeout: Timeout (po sek.) + setting_thumbnails_enabled: Rodyti sumažintus priedų atvaizdus + setting_thumbnails_size: Sumažinto atvaizdo dydis (taÅ¡keliais) + label_status_transitions: Darbų eiga + label_fields_permissions: Leidimai + label_readonly: Tik peržiÅ«ra + label_required: Privaloma(s) + text_repository_identifier_info: Leidžiamos tik mažosios raidÄ—s (a-z), skaitmenys, brÅ«kÅ¡neliai ir pabraukimo simboliai.
    KartÄ… iÅ¡saugojus pakeitimai negalimi + field_board_parent: Pagrindinis forumas + label_attribute_of_project: Projekto pavadinimas %{name} + label_attribute_of_author: Autorius %{name} + label_attribute_of_assigned_to: Paskirtas %{name} + label_attribute_of_fixed_version: Versijos %{name} + label_copy_subtasks: Kopijuoti darbo dalis + label_copied_to: kopijuota į + label_copied_from: kopijuota iÅ¡ + label_any_issues_in_project: bet kurie projekto darbai + label_any_issues_not_in_project: bet kurie ne Å¡io projekto darbai + field_private_notes: PrivaÄios žinutÄ—s + permission_view_private_notes: Matyti privaÄias žinutes + permission_set_notes_private: Pakeisti žinutÄ™ privaÄia + label_no_issues_in_project: projekte nÄ—ra darbų + label_any: visi + label_last_n_weeks: prieÅ¡ %{count} sav. + setting_cross_project_subtasks: Leisti susieti skirtingų projektų užduoÄių dalis + label_cross_project_descendants: Su sub-projektais + label_cross_project_tree: Su projekto medžiu + label_cross_project_hierarchy: Su projekto hierarchija + label_cross_project_system: Su visais projektais + button_hide: SlÄ—pti + setting_non_working_week_days: Nedarbo dienos + label_in_the_next_days: per ateinanÄias + label_in_the_past_days: per paskutines + label_attribute_of_user: Vartotojo %{name} + text_turning_multiple_off: Jei jÅ«s iÅ¡jungsite kelių reikÅ¡mių pasirinkimÄ…, visos iÅ¡vardintos reikÅ¡mÄ—s bus paÅ¡alintos ir palikta tik viena reikÅ¡mÄ— kiekvienam laukui. + label_attribute_of_issue: Ä®raÅ¡ai %{name} + permission_add_documents: PridÄ—ti dokumentus + permission_edit_documents: Redaguoti dokumentus + permission_delete_documents: Trinti dokumentus + label_gantt_progress_line: Progreso linija + setting_jsonp_enabled: Ä®galinti JSONP palaikymÄ… + field_inherit_members: PaveldÄ—ti narius + field_closed_on: Uždarytas + field_generate_password: Sugeneruoti slaptažodį + setting_default_projects_tracker_ids: Sekliai pagal nutylÄ—jimÄ… naujiems projektams + label_total_time: IÅ¡ viso + text_scm_config: JÅ«s galite pakeisti SCM komandas byloje config/configuration.yml. PraÅ¡ome perkrauti programÄ… po redagavimo, idant įgalinti pakeitimus. + text_scm_command_not_available: SCM komanda nepasiekiama. Patikrinkite administravimo skydelio nustatymus. + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/11389dcb8c49b70343e3b80fa2a021abf64753ca.svn-base --- a/.svn/pristine/11/11389dcb8c49b70343e3b80fa2a021abf64753ca.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingContextMenusTest < ActionController::IntegrationTest - def test_context_menus_time_entries - ["get", "post"].each do |method| - assert_routing( - { :method => method, :path => "/time_entries/context_menu" }, - { :controller => 'context_menus', :action => 'time_entries' } - ) - end - end - - def test_context_menus_issues - ["get", "post"].each do |method| - assert_routing( - { :method => method, :path => "/issues/context_menu" }, - { :controller => 'context_menus', :action => 'issues' } - ) - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/1143136ec6943c91201441e4e743a0264ddfa810.svn-base --- a/.svn/pristine/11/1143136ec6943c91201441e4e743a0264ddfa810.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -<%= form_tag({:action => 'edit', :tab => 'mail_handler'}) do %> - -
    -

    - <%= setting_text_area :mail_handler_body_delimiters, :rows => 5 %> - <%= l(:text_line_separated) %> -

    -
    - -
    -

    <%= setting_check_box :mail_handler_api_enabled, - :onclick => "if (this.checked) { $('#settings_mail_handler_api_key').removeAttr('disabled'); } else { $('#settings_mail_handler_api_key').attr('disabled', true); }"%>

    - -

    <%= setting_text_field :mail_handler_api_key, :size => 30, - :id => 'settings_mail_handler_api_key', - :disabled => !Setting.mail_handler_api_enabled? %> - <%= link_to_function l(:label_generate_key), "if (!$('#settings_mail_handler_api_key').attr('disabled')) { $('#settings_mail_handler_api_key').val(randomKey(20)) }" %> -

    -
    - -<%= submit_tag l(:button_save) %> - -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/1168184b847515eb2b5857914207c9183b271a1d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/1168184b847515eb2b5857914207c9183b271a1d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,93 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 ProjectsHelper + def link_to_version(version, options = {}) + return '' unless version && version.is_a?(Version) + link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options + end + + def project_settings_tabs + tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, + {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, + {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural}, + {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural}, + {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}, + {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki}, + {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural}, + {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}, + {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities} + ] + tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} + end + + def parent_project_select_tag(project) + selected = project.parent + # retrieve the requested parent project + parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id] + if parent_id + selected = (parent_id.blank? ? nil : Project.find(parent_id)) + end + + options = '' + options << "" if project.allowed_parents.include?(nil) + options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected) + content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id') + end + + def render_project_action_links + links = [] + links << link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') if User.current.allowed_to?(:add_project, nil, :global => true) + links << link_to(l(:label_issue_view_all), issues_path) if User.current.allowed_to?(:view_issues, nil, :global => true) + links << link_to(l(:label_overall_spent_time), time_entries_path) if User.current.allowed_to?(:view_time_entries, nil, :global => true) + links << link_to(l(:label_overall_activity), { :controller => 'activities', :action => 'index', :id => nil }) + links.join(" | ").html_safe + end + + # Renders the projects index + def render_project_hierarchy(projects) + render_project_nested_lists(projects) do |project| + s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}") + if project.description.present? + s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description') + end + s + end + end + + # Returns a set of options for a select field, grouped by project. + def version_options_for_select(versions, selected=nil) + grouped = Hash.new {|h,k| h[k] = []} + versions.each do |version| + grouped[version.project.name] << [version.name, version.id] + end + + selected = selected.is_a?(Version) ? selected.id : selected + if grouped.keys.size > 1 + grouped_options_for_select(grouped, selected) + else + options_for_select((grouped.values.first || []), selected) + end + end + + def format_version_sharing(sharing) + sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing) + l("label_version_sharing_#{sharing}") + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/1176a107aff15323df8f650ff2c14a601d13f571.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/1176a107aff15323df8f650ff2c14a601d13f571.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,38 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 PatchesTest < ActiveSupport::TestCase + include Redmine::I18n + + def setup + Setting.default_language = 'en' + end + + test "ActiveRecord::Base.human_attribute_name should transform name to field_name" do + assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on') + end + + test "ActiveRecord::Base.human_attribute_name should cut extra _id suffix for better validation" do + assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on_id') + end + + test "ActiveRecord::Base.human_attribute_name should default to humanized value if no translation has been found (useful for custom fields)" do + assert_equal 'Patch name', ActiveRecord::Base.human_attribute_name('Patch name') + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/11a5507f6b6077a6e73fb852952ddb0da8dc089e.svn-base --- a/.svn/pristine/11/11a5507f6b6077a6e73fb852952ddb0da8dc089e.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CustomFieldVersionFormatTest < ActiveSupport::TestCase - fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues, :versions - - def setup - @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'version') - end - - def test_possible_values_with_no_arguments - assert_equal [], @field.possible_values - assert_equal [], @field.possible_values(nil) - end - - def test_possible_values_with_project_resource - project = Project.find(1) - possible_values = @field.possible_values(project.issues.first) - assert possible_values.any? - assert_equal project.shared_versions.sort.collect(&:id).map(&:to_s), possible_values - end - - def test_possible_values_with_nil_project_resource - assert_equal [], @field.possible_values(Issue.new) - end - - def test_possible_values_options_with_no_arguments - assert_equal [], @field.possible_values_options - assert_equal [], @field.possible_values_options(nil) - end - - def test_possible_values_options_with_project_resource - project = Project.find(1) - possible_values_options = @field.possible_values_options(project.issues.first) - assert possible_values_options.any? - assert_equal project.shared_versions.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options - end - - def test_possible_values_options_with_array - projects = Project.find([1, 2]) - possible_values_options = @field.possible_values_options(projects) - assert possible_values_options.any? - assert_equal (projects.first.shared_versions & projects.last.shared_versions).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options - end - - def test_cast_blank_value - assert_equal nil, @field.cast_value(nil) - assert_equal nil, @field.cast_value("") - end - - def test_cast_valid_value - version = @field.cast_value("2") - assert_kind_of Version, version - assert_equal Version.find(2), version - end - - def test_cast_invalid_value - assert_equal nil, @field.cast_value("187") - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/11c16631f55588a98678a93f0bb375404c2e534c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/11c16631f55588a98678a93f0bb375404c2e534c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,33 @@ +# Settings specified here will take precedence over those in config/application.rb +RedmineApp::Application.configure do + # The production environment is meant for finished, "live" apps. + # Code is not reloaded between requests + config.cache_classes = true + + ##### + # Customize the default logger + # http://www.ruby-doc.org/stdlib-1.8.7/libdoc/logger/rdoc/Logger.html + # + # Use a different logger for distributed setups + # config.logger = SyslogLogger.new + # + # Rotate logs bigger than 1MB, keeps no more than 7 rotated logs around. + # When setting a new Logger, make sure to set it's log level too. + # + # config.logger = Logger.new(config.log_path, 7, 1048576) + # config.logger.level = Logger::INFO + + # Full error reports are disabled and caching is turned on + config.action_controller.perform_caching = true + + # Enable serving of images, stylesheets, and javascripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Disable delivery errors if you bad email addresses should just be ignored + config.action_mailer.raise_delivery_errors = false + + # No email in production log + config.action_mailer.logger = nil + + config.active_support.deprecation = :log +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/11c49a871d148b89615d8c37f2abca7b6149921b.svn-base --- a/.svn/pristine/11/11c49a871d148b89615d8c37f2abca7b6149921b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,377 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 IssueNestedSetTest < 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 - - def test_create_root_issue - issue1 = Issue.generate! - issue2 = Issue.generate! - issue1.reload - issue2.reload - - assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt] - assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt] - end - - def test_create_child_issue - parent = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent.id) - parent.reload - child.reload - - assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt] - assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt] - end - - def test_creating_a_child_in_a_subproject_should_validate - issue = Issue.generate! - child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1, - :subject => 'child', :parent_issue_id => issue.id) - assert_save child - assert_equal issue, child.reload.parent - end - - def test_creating_a_child_in_an_invalid_project_should_not_validate - issue = Issue.generate! - child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, - :subject => 'child', :parent_issue_id => issue.id) - assert !child.save - assert_not_nil child.errors[:parent_issue_id] - end - - def test_move_a_root_to_child - parent1 = Issue.generate! - parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - - parent2.parent_issue_id = parent1.id - parent2.save! - child.reload - parent1.reload - parent2.reload - - assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt] - end - - def test_move_a_child_to_root - parent1 = Issue.generate! - parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - - child.parent_issue_id = nil - child.save! - child.reload - parent1.reload - parent2.reload - - assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt] - end - - def test_move_a_child_to_another_issue - parent1 = Issue.generate! - parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - - child.parent_issue_id = parent2.id - child.save! - child.reload - parent1.reload - parent2.reload - - assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt] - end - - def test_move_a_child_with_descendants_to_another_issue - parent1 = Issue.generate! - parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - grandchild = Issue.generate!(:parent_issue_id => child.id) - - parent1.reload - parent2.reload - child.reload - grandchild.reload - - assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt] - assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] - - child.reload.parent_issue_id = parent2.id - child.save! - child.reload - grandchild.reload - parent1.reload - parent2.reload - - assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt] - assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] - end - - def test_move_a_child_with_descendants_to_another_project - parent1 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - grandchild = Issue.generate!(:parent_issue_id => child.id) - - child.reload - child.project = Project.find(2) - assert child.save - child.reload - grandchild.reload - parent1.reload - - assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt] - assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] - end - - def test_moving_an_issue_to_a_descendant_should_not_validate - parent1 = Issue.generate! - parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - grandchild = Issue.generate!(:parent_issue_id => child.id) - - child.reload - child.parent_issue_id = grandchild.id - assert !child.save - assert_not_nil child.errors[:parent_issue_id] - end - - def test_moving_an_issue_should_keep_valid_relations_only - issue1 = Issue.generate! - issue2 = Issue.generate! - issue3 = Issue.generate!(:parent_issue_id => issue2.id) - issue4 = Issue.generate! - r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES) - r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES) - r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES) - issue2.reload - issue2.parent_issue_id = issue1.id - issue2.save! - assert !IssueRelation.exists?(r1.id) - assert !IssueRelation.exists?(r2.id) - assert IssueRelation.exists?(r3.id) - end - - def test_destroy_should_destroy_children - issue1 = Issue.generate! - issue2 = Issue.generate! - issue3 = Issue.generate!(:parent_issue_id => issue2.id) - issue4 = Issue.generate!(:parent_issue_id => issue1.id) - - issue3.init_journal(User.find(2)) - issue3.subject = 'child with journal' - issue3.save! - - assert_difference 'Issue.count', -2 do - assert_difference 'Journal.count', -1 do - assert_difference 'JournalDetail.count', -1 do - Issue.find(issue2.id).destroy - end - end - end - - issue1.reload - issue4.reload - assert !Issue.exists?(issue2.id) - assert !Issue.exists?(issue3.id) - assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt] - assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt] - end - - def test_destroy_child_should_update_parent - issue = Issue.generate! - child1 = Issue.generate!(:parent_issue_id => issue.id) - child2 = Issue.generate!(:parent_issue_id => issue.id) - - issue.reload - assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt] - - child2.reload.destroy - - issue.reload - assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt] - end - - def test_destroy_parent_issue_updated_during_children_destroy - parent = Issue.generate! - Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id) - Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id) - - assert_difference 'Issue.count', -3 do - Issue.find(parent.id).destroy - end - end - - def test_destroy_child_issue_with_children - root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root') - child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id) - leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id) - leaf.init_journal(User.find(2)) - leaf.subject = 'leaf with journal' - leaf.save! - - assert_difference 'Issue.count', -2 do - assert_difference 'Journal.count', -1 do - assert_difference 'JournalDetail.count', -1 do - Issue.find(child.id).destroy - end - end - end - - root = Issue.find(root.id) - assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})" - end - - def test_destroy_issue_with_grand_child - parent = Issue.generate! - issue = Issue.generate!(:parent_issue_id => parent.id) - child = Issue.generate!(:parent_issue_id => issue.id) - grandchild1 = Issue.generate!(:parent_issue_id => child.id) - grandchild2 = Issue.generate!(:parent_issue_id => child.id) - - assert_difference 'Issue.count', -4 do - Issue.find(issue.id).destroy - parent.reload - assert_equal [1, 2], [parent.lft, parent.rgt] - end - end - - def test_parent_priority_should_be_the_highest_child_priority - parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal')) - # Create children - child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id) - assert_equal 'High', parent.reload.priority.name - child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id) - assert_equal 'Immediate', child1.reload.priority.name - assert_equal 'Immediate', parent.reload.priority.name - child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id) - assert_equal 'Immediate', parent.reload.priority.name - # Destroy a child - child1.destroy - assert_equal 'Low', parent.reload.priority.name - # Update a child - child3.reload.priority = IssuePriority.find_by_name('Normal') - child3.save! - assert_equal 'Normal', parent.reload.priority.name - end - - def test_parent_dates_should_be_lowest_start_and_highest_due_dates - parent = Issue.generate! - Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id) - Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id) - Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id) - parent.reload - assert_equal Date.parse('2010-01-25'), parent.start_date - assert_equal Date.parse('2010-02-22'), parent.due_date - end - - def test_parent_done_ratio_should_be_average_done_ratio_of_leaves - parent = Issue.generate! - Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id) - assert_equal 20, parent.reload.done_ratio - Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id) - assert_equal 45, parent.reload.done_ratio - - child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id) - assert_equal 30, parent.reload.done_ratio - - Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id) - assert_equal 30, child.reload.done_ratio - assert_equal 40, parent.reload.done_ratio - end - - def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any - parent = Issue.generate! - Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id) - assert_equal 20, parent.reload.done_ratio - Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id) - assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio - end - - def test_parent_estimate_should_be_sum_of_leaves - parent = Issue.generate! - Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id) - assert_equal nil, parent.reload.estimated_hours - Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id) - assert_equal 5, parent.reload.estimated_hours - Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id) - assert_equal 12, parent.reload.estimated_hours - end - - def test_move_parent_updates_old_parent_attributes - first_parent = Issue.generate! - second_parent = Issue.generate! - child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id) - assert_equal 5, first_parent.reload.estimated_hours - child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id) - assert_equal 7, second_parent.reload.estimated_hours - assert_nil first_parent.reload.estimated_hours - end - - def test_reschuling_a_parent_should_reschedule_subtasks - parent = Issue.generate! - c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id) - c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id) - parent.reload - parent.reschedule_on!(Date.parse('2010-06-02')) - c1.reload - assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date] - c2.reload - assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change - parent.reload - assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date] - end - - def test_project_copy_should_copy_issue_tree - p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2]) - i1 = Issue.generate!(:project => p, :subject => 'i1') - i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id) - i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id) - i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id) - i5 = Issue.generate!(:project => p, :subject => 'i5') - c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2]) - c.copy(p, :only => 'issues') - c.reload - - assert_equal 5, c.issues.count - ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject') - assert ic1.root? - assert_equal ic1, ic2.parent - assert_equal ic1, ic3.parent - assert_equal ic2, ic4.parent - assert ic5.root? - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/11e346e9edcfd6783f0cb16b00aae87132a43f72.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/11e346e9edcfd6783f0cb16b00aae87132a43f72.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,774 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 MailerTest < ActiveSupport::TestCase + include Redmine::I18n + include ActionDispatch::Assertions::SelectorAssertions + fixtures :projects, :enabled_modules, :issues, :users, :members, + :member_roles, :roles, :documents, :attachments, :news, + :tokens, :journals, :journal_details, :changesets, + :trackers, :projects_trackers, + :issue_statuses, :enumerations, :messages, :boards, :repositories, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, + :versions, + :comments + + def setup + ActionMailer::Base.deliveries.clear + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'http' + Setting.plain_text_mail = '0' + end + + def test_generated_links_in_emails + Setting.default_language = 'en' + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'https' + + journal = Journal.find(3) + assert Mailer.deliver_issue_edit(journal) + + mail = last_email + assert_not_nil mail + + assert_select_email do + # link to the main ticket + assert_select 'a[href=?]', + 'https://mydomain.foo/issues/2#change-3', + :text => 'Feature request #2: Add ingredients categories' + # link to a referenced ticket + assert_select 'a[href=?][title=?]', + 'https://mydomain.foo/issues/1', + "#{ESCAPED_UCANT} print recipes (New)", + :text => '#1' + # link to a changeset + assert_select 'a[href=?][title=?]', + 'https://mydomain.foo/projects/ecookbook/repository/revisions/2', + 'This commit fixes #1, #2 and references #1 & #3', + :text => 'r2' + # link to a description diff + assert_select 'a[href=?][title=?]', + 'https://mydomain.foo/journals/diff/3?detail_id=4', + 'View differences', + :text => 'diff' + # link to an attachment + assert_select 'a[href=?]', + 'https://mydomain.foo/attachments/download/4/source.rb', + :text => 'source.rb' + end + end + + def test_generated_links_with_prefix + Setting.default_language = 'en' + relative_url_root = Redmine::Utils.relative_url_root + Setting.host_name = 'mydomain.foo/rdm' + Setting.protocol = 'http' + + journal = Journal.find(3) + assert Mailer.deliver_issue_edit(journal) + + mail = last_email + assert_not_nil mail + + assert_select_email do + # link to the main ticket + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/issues/2#change-3', + :text => 'Feature request #2: Add ingredients categories' + # link to a referenced ticket + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/issues/1', + "#{ESCAPED_UCANT} print recipes (New)", + :text => '#1' + # link to a changeset + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2', + 'This commit fixes #1, #2 and references #1 & #3', + :text => 'r2' + # link to a description diff + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4', + 'View differences', + :text => 'diff' + # link to an attachment + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/attachments/download/4/source.rb', + :text => 'source.rb' + end + end + + def test_issue_edit_should_generate_url_with_hostname_for_relations + journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now) + journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2) + Mailer.deliver_issue_edit(journal) + assert_not_nil last_email + assert_select_email do + assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2' + end + end + + def test_generated_links_with_prefix_and_no_relative_url_root + Setting.default_language = 'en' + relative_url_root = Redmine::Utils.relative_url_root + Setting.host_name = 'mydomain.foo/rdm' + Setting.protocol = 'http' + Redmine::Utils.relative_url_root = nil + + journal = Journal.find(3) + assert Mailer.deliver_issue_edit(journal) + + mail = last_email + assert_not_nil mail + + assert_select_email do + # link to the main ticket + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/issues/2#change-3', + :text => 'Feature request #2: Add ingredients categories' + # link to a referenced ticket + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/issues/1', + "#{ESCAPED_UCANT} print recipes (New)", + :text => '#1' + # link to a changeset + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2', + 'This commit fixes #1, #2 and references #1 & #3', + :text => 'r2' + # link to a description diff + assert_select 'a[href=?][title=?]', + 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4', + 'View differences', + :text => 'diff' + # link to an attachment + assert_select 'a[href=?]', + 'http://mydomain.foo/rdm/attachments/download/4/source.rb', + :text => 'source.rb' + end + ensure + # restore it + Redmine::Utils.relative_url_root = relative_url_root + end + + def test_email_headers + issue = Issue.find(1) + Mailer.deliver_issue_add(issue) + mail = last_email + assert_not_nil mail + assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s + assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s + assert_equal '', mail.header['List-Id'].to_s + end + + def test_email_headers_should_include_sender + issue = Issue.find(1) + Mailer.deliver_issue_add(issue) + mail = last_email + assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s + end + + def test_plain_text_mail + Setting.plain_text_mail = 1 + journal = Journal.find(2) + Mailer.deliver_issue_edit(journal) + mail = last_email + assert_equal "text/plain; charset=UTF-8", mail.content_type + assert_equal 0, mail.parts.size + assert !mail.encoded.include?('href') + end + + def test_html_mail + Setting.plain_text_mail = 0 + journal = Journal.find(2) + Mailer.deliver_issue_edit(journal) + mail = last_email + assert_equal 2, mail.parts.size + assert mail.encoded.include?('href') + end + + def test_from_header + with_settings :mail_from => 'redmine@example.net' do + Mailer.test_email(User.find(1)).deliver + end + mail = last_email + assert_equal 'redmine@example.net', mail.from_addrs.first + end + + def test_from_header_with_phrase + with_settings :mail_from => 'Redmine app ' do + Mailer.test_email(User.find(1)).deliver + end + mail = last_email + assert_equal 'redmine@example.net', mail.from_addrs.first + assert_equal 'Redmine app ', mail.header['From'].to_s + end + + def test_should_not_send_email_without_recipient + news = News.first + user = news.author + # Remove members except news author + news.project.memberships.each {|m| m.destroy unless m.user == user} + + user.pref.no_self_notified = false + user.pref.save + User.current = user + Mailer.news_added(news.reload).deliver + assert_equal 1, last_email.bcc.size + + # nobody to notify + user.pref.no_self_notified = true + user.pref.save + User.current = user + ActionMailer::Base.deliveries.clear + Mailer.news_added(news.reload).deliver + assert ActionMailer::Base.deliveries.empty? + end + + def test_issue_add_message_id + issue = Issue.find(2) + Mailer.deliver_issue_add(issue) + mail = last_email + assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.issue-2.20060719190421@example.net", mail.references + end + + def test_issue_edit_message_id + journal = Journal.find(3) + journal.issue = Issue.find(2) + + Mailer.deliver_issue_edit(journal) + mail = last_email + assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.issue-2.20060719190421@example.net", mail.references + assert_select_email do + # link to the update + assert_select "a[href=?]", + "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}" + end + end + + def test_message_posted_message_id + message = Message.find(1) + Mailer.message_posted(message).deliver + mail = last_email + assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.message-1.20070512151532@example.net", mail.references + assert_select_email do + # link to the message + assert_select "a[href=?]", + "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}", + :text => message.subject + end + end + + def test_reply_posted_message_id + message = Message.find(3) + Mailer.message_posted(message).deliver + mail = last_email + assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.message-1.20070512151532@example.net", mail.references + assert_select_email do + # link to the reply + assert_select "a[href=?]", + "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}", + :text => message.subject + end + end + + test "#issue_add should notify project members" do + issue = Issue.find(1) + assert Mailer.deliver_issue_add(issue) + assert last_email.bcc.include?('dlopper@somenet.foo') + end + + test "#issue_add should not notify project members that are not allow to view the issue" do + issue = Issue.find(1) + Role.find(2).remove_permission!(:view_issues) + assert Mailer.deliver_issue_add(issue) + assert !last_email.bcc.include?('dlopper@somenet.foo') + end + + test "#issue_add should notify issue watchers" do + issue = Issue.find(1) + user = User.find(9) + # minimal email notification options + user.pref.no_self_notified = '1' + user.pref.save + user.mail_notification = false + user.save + + Watcher.create!(:watchable => issue, :user => user) + assert Mailer.deliver_issue_add(issue) + assert last_email.bcc.include?(user.mail) + end + + test "#issue_add should not notify watchers not allowed to view the issue" do + issue = Issue.find(1) + user = User.find(9) + Watcher.create!(:watchable => issue, :user => user) + Role.non_member.remove_permission!(:view_issues) + assert Mailer.deliver_issue_add(issue) + assert !last_email.bcc.include?(user.mail) + end + + def test_issue_add_should_include_enabled_fields + Setting.default_language = 'en' + issue = Issue.find(2) + assert Mailer.deliver_issue_add(issue) + assert_mail_body_match '* Target version: 1.0', last_email + assert_select_email do + assert_select 'li', :text => 'Target version: 1.0' + end + end + + def test_issue_add_should_not_include_disabled_fields + Setting.default_language = 'en' + issue = Issue.find(2) + tracker = issue.tracker + tracker.core_fields -= ['fixed_version_id'] + tracker.save! + assert Mailer.deliver_issue_add(issue) + assert_mail_body_no_match 'Target version', last_email + assert_select_email do + assert_select 'li', :text => /Target version/, :count => 0 + end + end + + # test mailer methods for each language + def test_issue_add + issue = Issue.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.deliver_issue_add(issue) + end + end + + def test_issue_edit + journal = Journal.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.deliver_issue_edit(journal) + end + end + + def test_issue_edit_should_send_private_notes_to_users_with_permission_only + journal = Journal.find(1) + journal.private_notes = true + journal.save! + + Role.find(2).add_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort + + Role.find(2).remove_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort + end + + def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only + Issue.find(1).set_watcher(User.find_by_login('someone')) + journal = Journal.find(1) + journal.private_notes = true + journal.save! + + Role.non_member.add_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort + + Role.non_member.remove_permission! :view_private_notes + Mailer.deliver_issue_edit(journal) + assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort + end + + def test_issue_edit_should_mark_private_notes + journal = Journal.find(2) + journal.private_notes = true + journal.save! + + with_settings :default_language => 'en' do + Mailer.deliver_issue_edit(journal) + end + assert_mail_body_match '(Private notes)', last_email + end + + def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue + issue = Issue.generate! + private_issue = Issue.generate!(:is_private => true) + IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates') + issue.reload + assert_equal 1, issue.journals.size + journal = issue.journals.first + ActionMailer::Base.deliveries.clear + + Mailer.deliver_issue_edit(journal) + last_email.bcc.each do |email| + user = User.find_by_mail(email) + assert private_issue.visible?(user), "Issue was not visible to #{user}" + end + end + + def test_document_added + document = Document.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.document_added(document).deliver + end + end + + def test_attachments_added + attachements = [ Attachment.find_by_container_type('Document') ] + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.attachments_added(attachements).deliver + end + end + + def test_version_file_added + attachements = [ Attachment.find_by_container_type('Version') ] + assert Mailer.attachments_added(attachements).deliver + assert_not_nil last_email.bcc + assert last_email.bcc.any? + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" + end + end + + def test_project_file_added + attachements = [ Attachment.find_by_container_type('Project') ] + assert Mailer.attachments_added(attachements).deliver + assert_not_nil last_email.bcc + assert last_email.bcc.any? + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" + end + end + + def test_news_added + news = News.first + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.news_added(news).deliver + end + end + + def test_news_added_should_notify_project_news_watchers + user1 = User.generate! + user2 = User.generate! + news = News.find(1) + news.project.enabled_module('news').add_watcher(user1) + + Mailer.news_added(news).deliver + assert_include user1.mail, last_email.bcc + assert_not_include user2.mail, last_email.bcc + end + + def test_news_comment_added + comment = Comment.find(2) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.news_comment_added(comment).deliver + end + end + + def test_message_posted + message = Message.first + recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author} + recipients = recipients.compact.uniq + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert Mailer.message_posted(message).deliver + end + end + + def test_wiki_content_added + content = WikiContent.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert_difference 'ActionMailer::Base.deliveries.size' do + assert Mailer.wiki_content_added(content).deliver + assert_select_email do + assert_select 'a[href=?]', + 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation', + :text => 'CookBook documentation' + end + end + end + end + + def test_wiki_content_updated + content = WikiContent.find(1) + valid_languages.each do |lang| + Setting.default_language = lang.to_s + assert_difference 'ActionMailer::Base.deliveries.size' do + assert Mailer.wiki_content_updated(content).deliver + assert_select_email do + assert_select 'a[href=?]', + 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation', + :text => 'CookBook documentation' + end + end + end + end + + def test_account_information + user = User.find(2) + valid_languages.each do |lang| + user.update_attribute :language, lang.to_s + user.reload + assert Mailer.account_information(user, 'pAsswORd').deliver + end + end + + def test_lost_password + token = Token.find(2) + valid_languages.each do |lang| + token.user.update_attribute :language, lang.to_s + token.reload + assert Mailer.lost_password(token).deliver + end + end + + def test_register + token = Token.find(1) + Setting.host_name = 'redmine.foo' + Setting.protocol = 'https' + + valid_languages.each do |lang| + token.user.update_attribute :language, lang.to_s + token.reload + ActionMailer::Base.deliveries.clear + assert Mailer.register(token).deliver + mail = last_email + assert_select_email do + assert_select "a[href=?]", + "https://redmine.foo/account/activate?token=#{token.value}", + :text => "https://redmine.foo/account/activate?token=#{token.value}" + end + end + end + + def test_test + user = User.find(1) + valid_languages.each do |lang| + user.update_attribute :language, lang.to_s + assert Mailer.test_email(user).deliver + end + end + + def test_reminders + Mailer.reminders(:days => 42) + assert_equal 1, ActionMailer::Base.deliveries.size + mail = last_email + assert mail.bcc.include?('dlopper@somenet.foo') + assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail + assert_equal '1 issue(s) due in the next 42 days', mail.subject + end + + def test_reminders_should_not_include_closed_issues + with_settings :default_language => 'en' do + Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5, + :subject => 'Closed issue', :assigned_to_id => 3, + :due_date => 5.days.from_now, + :author_id => 2) + ActionMailer::Base.deliveries.clear + + Mailer.reminders(:days => 42) + assert_equal 1, ActionMailer::Base.deliveries.size + mail = last_email + assert mail.bcc.include?('dlopper@somenet.foo') + assert_mail_body_no_match 'Closed issue', mail + end + end + + def test_reminders_for_users + Mailer.reminders(:days => 42, :users => ['5']) + assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper + Mailer.reminders(:days => 42, :users => ['3']) + assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper + mail = last_email + assert mail.bcc.include?('dlopper@somenet.foo') + assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail + end + + def test_reminder_should_include_issues_assigned_to_groups + with_settings :default_language => 'en' do + group = Group.generate! + group.users << User.find(2) + group.users << User.find(3) + + Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, + :subject => 'Assigned to group', :assigned_to => group, + :due_date => 5.days.from_now, + :author_id => 2) + ActionMailer::Base.deliveries.clear + + Mailer.reminders(:days => 7) + assert_equal 2, ActionMailer::Base.deliveries.size + assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort + ActionMailer::Base.deliveries.each do |mail| + assert_mail_body_match 'Assigned to group', mail + end + end + end + + def test_mailer_should_not_change_locale + Setting.default_language = 'en' + # Set current language to italian + set_language_if_valid 'it' + # Send an email to a french user + user = User.find(1) + user.language = 'fr' + Mailer.account_activated(user).deliver + mail = last_email + assert_mail_body_match 'Votre compte', mail + + assert_equal :it, current_language + end + + def test_with_deliveries_off + Mailer.with_deliveries false do + Mailer.test_email(User.find(1)).deliver + end + assert ActionMailer::Base.deliveries.empty? + # should restore perform_deliveries + assert ActionMailer::Base.perform_deliveries + end + + def test_token_for_should_strip_trailing_gt_from_address_with_full_name + with_settings :mail_from => "Redmine Mailer" do + assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!) + end + end + + def test_layout_should_include_the_emails_header + with_settings :emails_header => "*Header content*" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".header" do + assert_select "strong", :text => "Header content" + end + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_include "*Header content*", mail.body.decoded + end + end + end + + def test_layout_should_not_include_empty_emails_header + with_settings :emails_header => "", :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".header", false + end + end + end + + def test_layout_should_include_the_emails_footer + with_settings :emails_footer => "*Footer content*" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".footer" do + assert_select "strong", :text => "Footer content" + end + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_include "\n-- \n", mail.body.decoded + assert_include "*Footer content*", mail.body.decoded + end + end + end + + def test_layout_should_not_include_empty_emails_footer + with_settings :emails_footer => "" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".footer", false + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_not_include "\n-- \n", mail.body.decoded + end + end + end + + def test_should_escape_html_templates_only + Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a ') + mail = last_email + assert_equal 2, mail.parts.size + assert_include '', text_part.body.encoded + assert_include '<tag>', html_part.body.encoded + end + + def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true + mail = Mailer.test_email(User.find(1)) + mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) + + ActionMailer::Base.raise_delivery_errors = true + assert_raise Exception, "delivery error" do + mail.deliver + end + ensure + ActionMailer::Base.raise_delivery_errors = false + end + + def test_should_log_delivery_errors_when_raise_delivery_errors_is_false + mail = Mailer.test_email(User.find(1)) + mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) + + Rails.logger.expects(:error).with("Email delivery error: delivery error") + ActionMailer::Base.raise_delivery_errors = false + assert_nothing_raised do + mail.deliver + end + end + + def test_mail_should_return_a_mail_message + assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1)) + end + + private + + def last_email + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + mail + end + + def text_part + last_email.parts.detect {|part| part.content_type.include?('text/plain')} + end + + def html_part + last_email.parts.detect {|part| part.content_type.include?('text/html')} + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/11/11eb00b887f31790fc6e46fc8c8f7f620e8e7372.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/11eb00b887f31790fc6e46fc8c8f7f620e8e7372.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,199 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldsControllerTest < ActionController::TestCase + fixtures :custom_fields, :custom_values, :trackers, :users, :projects + + def setup + @request.session[:user_id] = 1 + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_new + custom_field_classes.each do |klass| + get :new, :type => klass.name + assert_response :success + assert_template 'new' + assert_kind_of klass, assigns(:custom_field) + assert_select 'form#custom_field_form' do + assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' + assert_select 'input[type=hidden][name=type][value=?]', klass.name + end + end + end + + def test_new_issue_custom_field + get :new, :type => 'IssueCustomField' + assert_response :success + assert_template 'new' + assert_select 'form#custom_field_form' do + assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' do + assert_select 'option[value=user]', :text => 'User' + assert_select 'option[value=version]', :text => 'Version' + end + assert_select 'input[type=checkbox][name=?]', 'custom_field[project_ids][]', Project.count + assert_select 'input[type=hidden][name=?]', 'custom_field[project_ids][]', 1 + assert_select 'input[type=hidden][name=type][value=IssueCustomField]' + end + end + + def test_new_time_entry_custom_field_should_not_show_trackers_and_projects + get :new, :type => 'TimeEntryCustomField' + assert_response :success + assert_template 'new' + assert_select 'form#custom_field_form' do + assert_select 'input[name=?]', 'custom_field[tracker_ids][]', 0 + assert_select 'input[name=?]', 'custom_field[project_ids][]', 0 + end + end + + def test_default_value_should_be_an_input_for_string_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'string'} + assert_response :success + assert_select 'input[name=?]', 'custom_field[default_value]' + end + + def test_default_value_should_be_a_textarea_for_text_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'text'} + assert_response :success + assert_select 'textarea[name=?]', 'custom_field[default_value]' + end + + def test_default_value_should_be_a_checkbox_for_bool_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'bool'} + assert_response :success + assert_select 'input[name=?][type=checkbox]', 'custom_field[default_value]' + end + + def test_default_value_should_not_be_present_for_user_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'user'} + assert_response :success + assert_select '[name=?]', 'custom_field[default_value]', 0 + end + + def test_new_js + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'list'}, :format => 'js' + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + + field = assigns(:custom_field) + assert_equal 'list', field.field_format + end + + def test_new_with_invalid_custom_field_class_should_render_404 + get :new, :type => 'UnknownCustomField' + assert_response 404 + end + + def test_create_list_custom_field + assert_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", + :custom_field => {:name => "test_post_new_list", + :default_value => "", + :min_length => "0", + :searchable => "0", + :regexp => "", + :is_for_all => "1", + :possible_values => "0.1\n0.2\n", + :max_length => "0", + :is_filter => "0", + :is_required =>"0", + :field_format => "list", + :tracker_ids => ["1", ""]} + end + assert_redirected_to '/custom_fields?tab=IssueCustomField' + field = IssueCustomField.find_by_name('test_post_new_list') + assert_not_nil field + assert_equal ["0.1", "0.2"], field.possible_values + assert_equal 1, field.trackers.size + end + + def test_create_with_project_ids + assert_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", :custom_field => { + :name => "foo", :field_format => "string", :is_for_all => "0", :project_ids => ["1", "3", ""] + } + assert_response 302 + end + field = IssueCustomField.order("id desc").first + assert_equal [1, 3], field.projects.map(&:id).sort + end + + def test_create_with_failure + assert_no_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", :custom_field => {:name => ''} + end + assert_response :success + assert_template 'new' + end + + def test_edit + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_tag 'input', :attributes => {:name => 'custom_field[name]', :value => 'Database'} + end + + def test_edit_invalid_custom_field_should_render_404 + get :edit, :id => 99 + assert_response 404 + end + + def test_update + put :update, :id => 1, :custom_field => {:name => 'New name'} + assert_redirected_to '/custom_fields?tab=IssueCustomField' + + field = CustomField.find(1) + assert_equal 'New name', field.name + end + + def test_update_with_failure + put :update, :id => 1, :custom_field => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + custom_values_count = CustomValue.where(:custom_field_id => 1).count + assert custom_values_count > 0 + + assert_difference 'CustomField.count', -1 do + assert_difference 'CustomValue.count', - custom_values_count do + delete :destroy, :id => 1 + end + end + + assert_redirected_to '/custom_fields?tab=IssueCustomField' + assert_nil CustomField.find_by_id(1) + assert_nil CustomValue.find_by_custom_field_id(1) + end + + def custom_field_classes + files = Dir.glob(File.join(Rails.root, 'app/models/*_custom_field.rb')).map {|f| File.basename(f).sub(/\.rb$/, '') } + classes = files.map(&:classify).map(&:constantize) + assert classes.size > 0 + classes + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/120969c860929db9fea57c7df9319d9ee6532d44.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/120969c860929db9fea57c7df9319d9ee6532d44.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,43 @@ +api.array :custom_fields do + @custom_fields.each do |field| + api.custom_field do + api.id field.id + api.name field.name + api.customized_type field.class.customized_class.name.underscore if field.class.customized_class + api.field_format field.field_format + api.regexp field.regexp + api.min_length field.min_length + api.max_length field.max_length + api.is_required field.is_required? + api.is_filter field.is_filter? + api.searchable field.searchable + api.multiple field.multiple? + api.default_value field.default_value + api.visible field.visible? + + values = field.possible_values_options + if values.present? + api.array :possible_values do + values.each do |label, value| + api.possible_value do + api.value value || label + end + end + end + end + + if field.is_a?(IssueCustomField) + api.array :trackers do + field.trackers.each do |tracker| + api.tracker :id => tracker.id, :name => tracker.name + end + end + api.array :roles do + field.roles.each do |role| + api.role :id => role.id, :name => role.name + end + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/120f067fdcb658f3716efac6a31070d12015a618.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/120f067fdcb658f3716efac6a31070d12015a618.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,53 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CommentsController < ApplicationController + default_search_scope :news + model_object News + before_filter :find_model_object + before_filter :find_project_from_association + before_filter :authorize + + def create + raise Unauthorized unless @news.commentable? + + @comment = Comment.new + @comment.safe_attributes = params[:comment] + @comment.author = User.current + if @news.comments << @comment + flash[:notice] = l(:label_comment_added) + end + + redirect_to news_path(@news) + end + + def destroy + @news.comments.find(params[:comment_id]).destroy + redirect_to news_path(@news) + end + + private + + # ApplicationController's find_model_object sets it based on the controller + # name so it needs to be overriden and set to @news instead + def find_model_object + super + @news = @object + @comment = nil + @news + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/121eeed4fcb10e023ef41b53336708e73d10e3e6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/121eeed4fcb10e023ef41b53336708e73d10e3e6.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,38 @@ +<%= form_tag({:action => 'edit', :tab => 'authentication'}) do %> + +
    +

    <%= setting_check_box :login_required %>

    + +

    <%= setting_select :autologin, [[l(:label_disabled), 0]] + [1, 7, 30, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]} %>

    + +

    <%= setting_select :self_registration, [[l(:label_disabled), "0"], + [l(:label_registration_activation_by_email), "1"], + [l(:label_registration_manual_activation), "2"], + [l(:label_registration_automatic_activation), "3"]] %>

    + +

    <%= setting_check_box :unsubscribe %>

    + +

    <%= setting_text_field :password_min_length, :size => 6 %>

    + +

    <%= setting_check_box :lost_password, :label => :label_password_lost %>

    + +

    <%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %>

    + +

    <%= setting_check_box :rest_api_enabled %>

    + +

    <%= setting_check_box :jsonp_enabled %>

    +
    + +
    + <%= l(:label_session_expiration) %> + +
    +

    <%= setting_select :session_lifetime, [[l(:label_disabled), 0]] + [1, 7, 30, 60, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), (days * 60 * 24).to_s]} %>

    +

    <%= setting_select :session_timeout, [[l(:label_disabled), 0]] + [1, 2, 4, 8, 12, 24, 48].collect{|hours| [l('datetime.distance_in_words.x_hours', :count => hours), (hours * 60).to_s]} %>

    +
    + +

    <%= l(:text_session_expiration_settings) %>

    +
    + +<%= submit_tag l(:button_save) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/1250d373a782657c3a0120825ac45c4635fdccf2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/1250d373a782657c3a0120825ac45c4635fdccf2.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,7 @@ +--- a.txt 2013-07-27 05:52:11.415223830 +0900 ++++ b.txt 2013-07-27 05:52:18.249190358 +0900 +@@ -1,3 +1,3 @@ + aaaa +-日本記ok ++日本誘ok + bbbb diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/1255b1abb30e02b5520a0d75d153507b1306bc5d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/1255b1abb30e02b5520a0d75d153507b1306bc5d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,57 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CommentTest < ActiveSupport::TestCase + fixtures :users, :news, :comments, :projects, :enabled_modules + + def setup + @jsmith = User.find(2) + @news = News.find(1) + end + + def test_create + comment = Comment.new(:commented => @news, :author => @jsmith, :comments => "my comment") + assert comment.save + @news.reload + assert_equal 2, @news.comments_count + end + + def test_create_should_send_notification + Watcher.create!(:watchable => @news, :user => @jsmith) + + with_settings :notified_events => %w(news_comment_added) do + assert_difference 'ActionMailer::Base.deliveries.size' do + Comment.create!(:commented => @news, :author => @jsmith, :comments => "my comment") + end + end + end + + def test_validate + comment = Comment.new(:commented => @news) + assert !comment.save + assert_equal 2, comment.errors.count + end + + def test_destroy + comment = Comment.find(1) + assert comment.destroy + @news.reload + assert_equal 0, @news.comments_count + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/129828b625f30815a786616d216ef1dba321d685.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/129828b625f30815a786616d216ef1dba321d685.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,101 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 WikiTest < ActiveSupport::TestCase + fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions + + def test_create + wiki = Wiki.new(:project => Project.find(2)) + assert !wiki.save + assert_equal 1, wiki.errors.count + + wiki.start_page = "Start page" + assert wiki.save + end + + def test_update + @wiki = Wiki.find(1) + @wiki.start_page = "Another start page" + assert @wiki.save + @wiki.reload + assert_equal "Another start page", @wiki.start_page + end + + def test_find_page_should_not_be_case_sensitive + wiki = Wiki.find(1) + page = WikiPage.find(2) + + assert_equal page, wiki.find_page('Another_page') + assert_equal page, wiki.find_page('Another page') + assert_equal page, wiki.find_page('ANOTHER page') + end + + def test_find_page_with_cyrillic_characters + wiki = Wiki.find(1) + page = WikiPage.find(10) + assert_equal page, wiki.find_page('Этика_менеджмента') + end + + def test_find_page_with_backslashes + wiki = Wiki.find(1) + page = WikiPage.create!(:wiki => wiki, :title => '2009\\02\\09') + assert_equal page, wiki.find_page('2009\\02\\09') + end + + def test_find_page_without_redirect + wiki = Wiki.find(1) + page = wiki.find_page('Another_page') + assert_not_nil page + assert_equal 'Another_page', page.title + assert_equal false, wiki.page_found_with_redirect? + end + + def test_find_page_with_redirect + wiki = Wiki.find(1) + WikiRedirect.create!(:wiki => wiki, :title => 'Old_title', :redirects_to => 'Another_page') + page = wiki.find_page('Old_title') + assert_not_nil page + assert_equal 'Another_page', page.title + assert_equal true, wiki.page_found_with_redirect? + end + + def test_titleize + ja_test = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" + ja_test.force_encoding('UTF-8') if ja_test.respond_to?(:force_encoding) + assert_equal 'Page_title_with_CAPITALES', Wiki.titleize('page title with CAPITALES') + assert_equal ja_test, Wiki.titleize(ja_test) + end + + def test_sidebar_should_return_nil_if_undefined + @wiki = Wiki.find(1) + assert_nil @wiki.sidebar + end + + def test_sidebar_should_return_a_wiki_page_if_defined + @wiki = Wiki.find(1) + page = @wiki.pages.new(:title => 'Sidebar') + page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar') + page.save! + + assert_kind_of WikiPage, @wiki.sidebar + assert_equal 'Sidebar', @wiki.sidebar.title + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/12e2bd6b115cd00dd763ce32dbfae0382d38cea8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/12e2bd6b115cd00dd763ce32dbfae0382d38cea8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,183 @@ +desc 'Updates and checks locales against en.yml' +task :locales do + %w(locales:update locales:check_interpolation).collect do |task| + Rake::Task[task].invoke + end +end + +namespace :locales do + desc 'Updates language files based on en.yml content (only works for new top level keys).' + task :update do + dir = ENV['DIR'] || './config/locales' + + en_strings = YAML.load(File.read(File.join(dir,'en.yml')))['en'] + + files = Dir.glob(File.join(dir,'*.{yaml,yml}')) + files.sort.each do |file| + puts "Updating file #{file}" + file_strings = YAML.load(File.read(file)) + file_strings = file_strings[file_strings.keys.first] + + missing_keys = en_strings.keys - file_strings.keys + next if missing_keys.empty? + + puts "==> Missing #{missing_keys.size} keys (#{missing_keys.join(', ')})" + lang = File.open(file, 'a') + + missing_keys.each do |key| + {key => en_strings[key]}.to_yaml.each_line do |line| + next if line =~ /^---/ || line.empty? + puts " #{line}" + lang << " #{line}" + end + end + + lang.close + end + end + + desc 'Checks interpolation arguments in locals against en.yml' + task :check_interpolation do + dir = ENV['DIR'] || './config/locales' + en_strings = YAML.load(File.read(File.join(dir,'en.yml')))['en'] + files = Dir.glob(File.join(dir,'*.{yaml,yml}')) + files.sort.each do |file| + puts "parsing #{file}..." + file_strings = YAML.load_file(file) + unless file_strings.is_a?(Hash) + puts "#{file}: content is not a Hash (#{file_strings.class.name})" + next + end + unless file_strings.keys.size == 1 + puts "#{file}: content has multiple keys (#{file_strings.keys.size})" + next + end + file_strings = file_strings[file_strings.keys.first] + + file_strings.each do |key, string| + next unless string.is_a?(String) + string.scan /%\{\w+\}/ do |match| + unless en_strings[key].nil? || en_strings[key].include?(match) + puts "#{file}: #{key} uses #{match} not found in en.yml" + end + end + end + end + end + + desc <<-END_DESC +Removes a translation string from all locale file (only works for top-level childless non-multiline keys, probably doesn\'t work on windows). + +This task does not work on Ruby 1.8.6. +You need to use Ruby 1.8.7 or later. + +Options: + key=key_1,key_2 Comma-separated list of keys to delete + skip=en,de Comma-separated list of locale files to ignore (filename without extension) +END_DESC + + task :remove_key do + dir = ENV['DIR'] || './config/locales' + files = Dir.glob(File.join(dir,'*.yml')) + skips = ENV['skip'] ? Regexp.union(ENV['skip'].split(',')) : nil + deletes = ENV['key'] ? Regexp.union(ENV['key'].split(',')) : nil + # Ignore multiline keys (begin with | or >) and keys with children (nothing meaningful after :) + delete_regex = /\A #{deletes}: +[^\|>\s#].*\z/ + + files.each do |path| + # Skip certain locales + (puts "Skipping #{path}"; next) if File.basename(path, ".yml") =~ skips + puts "Deleting selected keys from #{path}" + orig_content = File.open(path, 'r') {|file| file.read} + File.open(path, 'w') {|file| orig_content.each_line {|line| file.puts line unless line.chomp =~ delete_regex}} + end + end + + desc <<-END_DESC +Adds a new top-level translation string to all locale file (only works for childless keys, probably doesn\'t work on windows, doesn't check for duplicates). + +Options: + key="some_key=foo" + key1="another_key=bar" + key_fb="foo=bar" Keys to add in the form key=value, every option of the form key[,\\d,_*] will be recognised + skip=en,de Comma-separated list of locale files to ignore (filename without extension) +END_DESC + + task :add_key do + dir = ENV['DIR'] || './config/locales' + files = Dir.glob(File.join(dir,'*.yml')) + skips = ENV['skip'] ? Regexp.union(ENV['skip'].split(',')) : nil + keys_regex = /\Akey(\d+|_.+)?\z/ + adds = ENV.reject {|k,v| !(k =~ keys_regex)}.values.collect {|v| Array.new v.split("=",2)} + key_list = adds.collect {|v| v[0]}.join(", ") + + files.each do |path| + # Skip certain locales + (puts "Skipping #{path}"; next) if File.basename(path, ".yml") =~ skips + # TODO: Check for dupliate/existing keys + puts "Adding #{key_list} to #{path}" + File.open(path, 'a') do |file| + adds.each do |kv| + Hash[*kv].to_yaml.each_line do |line| + file.puts " #{line}" unless (line =~ /^---/ || line.empty?) + end + end + end + end + end + + desc 'Duplicates a key. Exemple rake locales:dup key=foo new_key=bar' + task :dup do + dir = ENV['DIR'] || './config/locales' + files = Dir.glob(File.join(dir,'*.yml')) + skips = ENV['skip'] ? Regexp.union(ENV['skip'].split(',')) : nil + key = ENV['key'] + new_key = ENV['new_key'] + abort "Missing key argument" if key.blank? + abort "Missing new_key argument" if new_key.blank? + + files.each do |path| + # Skip certain locales + (puts "Skipping #{path}"; next) if File.basename(path, ".yml") =~ skips + puts "Adding #{new_key} to #{path}" + + strings = File.read(path) + unless strings =~ /^( #{key}: .+)$/ + puts "Key not found in #{path}" + next + end + line = $1 + + File.open(path, 'a') do |file| + file.puts(line.sub(key, new_key)) + end + end + end + + desc 'Check parsing yaml by psych library on Ruby 1.9.' + + # On Fedora 12 and 13, if libyaml-devel is available, + # in case of installing by rvm, + # Ruby 1.9 default yaml library is psych. + + task :check_parsing_by_psych do + begin + require 'psych' + parser = Psych::Parser.new + dir = ENV['DIR'] || './config/locales' + files = Dir.glob(File.join(dir,'*.yml')) + files.sort.each do |filename| + next if File.directory? filename + puts "parsing #{filename}..." + begin + parser.parse File.open(filename) + rescue Exception => e1 + puts(e1.message) + puts("") + end + end + rescue Exception => e + puts(e.message) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/12ef2ae313a0833c5606f02c0f4c890cafaa550f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/12ef2ae313a0833c5606f02c0f4c890cafaa550f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,36 @@ +desc 'Load Redmine default configuration data. Language is chosen interactively or by setting REDMINE_LANG environment variable.' + +namespace :redmine do + task :load_default_data => :environment do + require 'custom_field' + include Redmine::I18n + set_language_if_valid('en') + + envlang = ENV['REDMINE_LANG'] + if !envlang || !set_language_if_valid(envlang) + puts + while true + print "Select language: " + print valid_languages.collect(&:to_s).sort.join(", ") + print " [#{current_language}] " + STDOUT.flush + lang = STDIN.gets.chomp! + break if lang.empty? + break if set_language_if_valid(lang) + puts "Unknown language!" + end + STDOUT.flush + puts "====================================" + end + + begin + Redmine::DefaultData::Loader.load(current_language) + puts "Default configuration data loaded." + rescue Redmine::DefaultData::DataAlreadyLoaded => error + puts error.message + rescue => error + puts "Error: " + error.message + puts "Default configuration data was not loaded." + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/12/12f45ed7c5d768c433966a08f88e9ccd503d9a14.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/12f45ed7c5d768c433966a08f88e9ccd503d9a14.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1104 @@ +# Redmine EU language +# Author: Ales Zabala Alava (Shagi), +# 2010-01-25 +# Distributed under the same terms as the Redmine itself. +eu: + direction: ltr + 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: [Igandea, Astelehena, Asteartea, Asteazkena, Osteguna, Ostirala, Larunbata] + abbr_day_names: [Ig., Al., Ar., Az., Og., Or., La.] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Urtarrila, Otsaila, Martxoa, Apirila, Maiatza, Ekaina, Uztaila, Abuztua, Iraila, Urria, Azaroa, Abendua] + abbr_month_names: [~, Urt, Ots, Mar, Api, Mai, Eka, Uzt, Abu, Ira, Urr, Aza, Abe] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y/%m/%d %H:%M" + time: "%H:%M" + short: "%b %d %H:%M" + long: "%Y %B %d %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "minutu erdi" + less_than_x_seconds: + one: "segundu bat baino gutxiago" + other: "%{count} segundu baino gutxiago" + x_seconds: + one: "segundu 1" + other: "%{count} segundu" + less_than_x_minutes: + one: "minutu bat baino gutxiago" + other: "%{count} minutu baino gutxiago" + x_minutes: + one: "minutu 1" + other: "%{count} minutu" + about_x_hours: + one: "ordu 1 inguru" + other: "%{count} ordu inguru" + x_hours: + one: "ordu 1" + other: "%{count} ordu" + x_days: + one: "egun 1" + other: "%{count} egun" + about_x_months: + one: "hilabete 1 inguru" + other: "%{count} hilabete inguru" + x_months: + one: "hilabete 1" + other: "%{count} hilabete" + about_x_years: + one: "urte 1 inguru" + other: "%{count} urte inguru" + over_x_years: + one: "urte 1 baino gehiago" + other: "%{count} urte baino gehiago" + almost_x_years: + one: "ia urte 1" + other: "ia %{count} urte" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Byte" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "eta" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "Errore batek %{model} hau godetzea galarazi du." + other: "%{count} errorek %{model} hau gordetzea galarazi dute." + messages: + inclusion: "ez dago zerrendan" + exclusion: "erreserbatuta dago" + invalid: "baliogabea da" + confirmation: "ez du berrespenarekin bat egiten" + accepted: "onartu behar da" + empty: "ezin da hutsik egon" + blank: "ezin da hutsik egon" + too_long: "luzeegia da (maximoa %{count} karaktere dira)" + too_short: "laburregia da (minimoa %{count} karaktere dira)" + wrong_length: "luzera ezegokia da (%{count} karakter izan beharko litzake)" + taken: "dagoeneko hartuta dago" + not_a_number: "ez da zenbaki bat" + not_a_date: "ez da baliozko data" + greater_than: "%{count} baino handiagoa izan behar du" + greater_than_or_equal_to: "%{count} edo handiagoa izan behar du" + equal_to: "%{count} izan behar du" + less_than: "%{count} baino gutxiago izan behar du" + less_than_or_equal_to: "%{count} edo gutxiago izan behar du" + odd: "bakoitia izan behar du" + even: "bikoitia izan behar du" + greater_than_start_date: "hasiera data baino handiagoa izan behar du" + not_same_project: "ez dago proiektu berdinean" + circular_dependency: "Erlazio honek mendekotasun zirkular bat sortuko luke" + cant_link_an_issue_with_a_descendant: "Zeregin bat ezin da bere azpiataza batekin estekatu." + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Hautatu mesedez + + general_text_No: 'Ez' + general_text_Yes: 'Bai' + general_text_no: 'ez' + general_text_yes: 'bai' + general_lang_name: 'Euskara' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Kontua ongi eguneratu da. + notice_account_invalid_creditentials: Erabiltzaile edo pasahitz ezegokia + notice_account_password_updated: Pasahitza ongi eguneratu da. + notice_account_wrong_password: Pasahitz ezegokia. + notice_account_register_done: Kontua ongi sortu da. Kontua gaitzeko klikatu epostan adierazi zaizun estekan. + notice_account_unknown_email: Erabiltzaile ezezaguna. + notice_can_t_change_password: Kontu honek kanpoko autentikazio bat erabiltzen du. Ezinezkoa da pasahitza aldatzea. + notice_account_lost_email_sent: Pasahitz berria aukeratzeko jarraibideak dituen eposta bat bidali zaizu. + notice_account_activated: Zure kontua gaituta dago. Orain saioa has dezakezu + notice_successful_create: Sortze arrakastatsua. + notice_successful_update: Eguneratze arrakastatsua. + notice_successful_delete: Ezabaketa arrakastatsua. + notice_successful_connection: Konexio arrakastatsua. + notice_file_not_found: Atzitu nahi duzun orria ez da exisitzen edo ezabatua izan da. + notice_locking_conflict: Beste erabiltzaile batek datuak eguneratu ditu. + notice_not_authorized: Ez duzu orri hau atzitzeko baimenik. + notice_email_sent: "%{value} helbidera eposta bat bidali da" + notice_email_error: "Errorea eposta bidaltzean (%{value})" + notice_feeds_access_key_reseted: Zure Atom atzipen giltza berrezarri da. + notice_api_access_key_reseted: Zure API atzipen giltza berrezarri da. + notice_failed_to_save_issues: "Hautatutako %{total} zereginetatik %{count} ezin izan dira konpondu: %{ids}." + notice_no_issue_selected: "Ez da zereginik hautatu! Mesedez, editatu nahi dituzun arazoak markatu." + notice_account_pending: "Zure kontua sortu da, orain kudeatzailearen onarpenaren zain dago." + notice_default_data_loaded: Lehenetsitako konfigurazioa ongi kargatu da. + notice_unable_delete_version: Ezin da bertsioa ezabatu. + notice_issue_done_ratios_updated: Burututako zereginen erlazioa eguneratu da. + + error_can_t_load_default_data: "Ezin izan da lehenetsitako konfigurazioa kargatu: %{value}" + error_scm_not_found: "Sarrera edo berrikuspena ez da biltegian topatu." + error_scm_command_failed: "Errorea gertatu da biltegia atzitzean: %{value}" + error_scm_annotate: "Sarrera ez da existitzen edo ezin da anotatu." + error_issue_not_found_in_project: 'Zeregina ez da topatu edo ez da proiektu honetakoa' + error_no_tracker_in_project: 'Proiektu honek ez du aztarnaririk esleituta. Mesedez egiaztatu Proiektuaren ezarpenak.' + error_no_default_issue_status: 'Zereginek ez dute lehenetsitako egoerarik. Mesedez egiaztatu zure konfigurazioa ("Kudeaketa -> Arazoen egoerak" atalera joan).' + error_can_not_reopen_issue_on_closed_version: 'Itxitako bertsio batera esleitutako zereginak ezin dira berrireki' + error_can_not_archive_project: Proiektu hau ezin da artxibatu + error_issue_done_ratios_not_updated: "Burututako zereginen erlazioa ez da eguneratu." + error_workflow_copy_source: 'Mesedez hautatu iturburuko aztarnari edo rola' + error_workflow_copy_target: 'Mesedez hautatu helburuko aztarnari(ak) edo rola(k)' + + warning_attachments_not_saved: "%{count} fitxategi ezin izan d(ir)a gorde." + + mail_subject_lost_password: "Zure %{value} pasahitza" + mail_body_lost_password: 'Zure pasahitza aldatzeko hurrengo estekan klikatu:' + mail_subject_register: "Zure %{value} kontuaren gaitzea" + mail_body_register: 'Zure kontua gaitzeko hurrengo estekan klikatu:' + mail_body_account_information_external: "Zure %{value} kontua erabil dezakezu saioa hasteko." + mail_body_account_information: Zure kontuaren informazioa + mail_subject_account_activation_request: "%{value} kontu gaitzeko eskaera" + mail_body_account_activation_request: "Erabiltzaile berri bat (%{value}) erregistratu da. Kontua zure onarpenaren zain dago:" + mail_subject_reminder: "%{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a" + mail_body_reminder: "Zuri esleituta dauden %{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a:" + mail_subject_wiki_content_added: "'%{id}' wiki orria gehitu da" + mail_body_wiki_content_added: "%{author}-(e)k '%{id}' wiki orria gehitu du." + mail_subject_wiki_content_updated: "'%{id}' wiki orria eguneratu da" + mail_body_wiki_content_updated: "%{author}-(e)k '%{id}' wiki orria eguneratu du." + + + field_name: Izena + field_description: Deskribapena + field_summary: Laburpena + field_is_required: Beharrezkoa + field_firstname: Izena + field_lastname: Abizenak + field_mail: Eposta + field_filename: Fitxategia + field_filesize: Tamaina + field_downloads: Deskargak + field_author: Egilea + field_created_on: Sortuta + field_updated_on: Eguneratuta + field_field_format: Formatua + field_is_for_all: Proiektu guztietarako + field_possible_values: Balio posibleak + field_regexp: Expresio erregularra + field_min_length: Luzera minimoa + field_max_length: Luzera maxioma + field_value: Balioa + field_category: Kategoria + field_title: Izenburua + field_project: Proiektua + field_issue: Zeregina + field_status: Egoera + field_notes: Oharrak + field_is_closed: Itxitako arazoa + field_is_default: Lehenetsitako balioa + field_tracker: Aztarnaria + field_subject: Gaia + field_due_date: Amaiera data + field_assigned_to: Esleituta + field_priority: Lehentasuna + field_fixed_version: Helburuko bertsioa + field_user: Erabiltzilea + field_role: Rola + field_homepage: Orri nagusia + field_is_public: Publikoa + field_parent: "Honen azpiproiektua:" + field_is_in_roadmap: Arazoak ibilbide-mapan erakutsi + field_login: Erabiltzaile izena + field_mail_notification: Eposta jakinarazpenak + field_admin: Kudeatzailea + field_last_login_on: Azken konexioa + field_language: Hizkuntza + field_effective_date: Data + field_password: Pasahitza + field_new_password: Pasahitz berria + field_password_confirmation: Berrespena + field_version: Bertsioa + field_type: Mota + field_host: Ostalaria + field_port: Portua + field_account: Kontua + field_base_dn: Base DN + field_attr_login: Erabiltzaile atributua + field_attr_firstname: Izena atributua + field_attr_lastname: Abizenak atributua + field_attr_mail: Eposta atributua + field_onthefly: Zuzeneko erabiltzaile sorrera + field_start_date: Hasiera + field_done_ratio: Egindako % + field_auth_source: Autentikazio modua + field_hide_mail: Nire eposta helbidea ezkutatu + field_comments: Iruzkina + field_url: URL + field_start_page: Hasierako orria + field_subproject: Azpiproiektua + field_hours: Ordu + field_activity: Jarduera + field_spent_on: Data + field_identifier: Identifikatzailea + field_is_filter: Iragazki moduan erabilita + field_issue_to: Erlazionatutako zereginak + field_delay: Atzerapena + field_assignable: Arazoak rol honetara esleitu daitezke + field_redirect_existing_links: Existitzen diren estekak berbideratu + field_estimated_hours: Estimatutako denbora + field_column_names: Zutabeak + field_time_zone: Ordu zonaldea + field_searchable: Bilagarria + field_default_value: Lehenetsitako balioa + field_comments_sorting: Iruzkinak erakutsi + field_parent_title: Orri gurasoa + field_editable: Editagarria + field_watcher: Behatzailea + field_identity_url: OpenID URLa + field_content: Edukia + field_group_by: Emaitzak honegatik taldekatu + field_sharing: Partekatzea + + setting_app_title: Aplikazioaren izenburua + setting_app_subtitle: Aplikazioaren azpizenburua + setting_welcome_text: Ongietorriko testua + setting_default_language: Lehenetsitako hizkuntza + setting_login_required: Autentikazioa derrigorrezkoa + setting_self_registration: Norberak erregistratu + setting_attachment_max_size: Eranskinen tamaina max. + setting_issues_export_limit: Zereginen esportatze limitea + setting_mail_from: Igorlearen eposta helbidea + setting_bcc_recipients: Hartzaileak ezkutuko kopian (bcc) + setting_plain_text_mail: Testu soileko epostak (HTML-rik ez) + setting_host_name: Ostalari izena eta bidea + setting_text_formatting: Testu formatua + setting_wiki_compression: Wikiaren historia konprimitu + setting_feeds_limit: Jarioaren edukiera limitea + setting_default_projects_public: Proiektu berriak defektuz publikoak dira + setting_autofetch_changesets: Commit-ak automatikoki hartu + setting_sys_api_enabled: Biltegien kudeaketarako WS gaitu + setting_commit_ref_keywords: Erreferentzien gako-hitzak + setting_commit_fix_keywords: Konpontze gako-hitzak + setting_autologin: Saioa automatikoki hasi + setting_date_format: Data formatua + setting_time_format: Ordu formatua + setting_cross_project_issue_relations: Zereginak proiektuen artean erlazionatzea baimendu + setting_issue_list_default_columns: Zereginen zerrendan defektuz ikusten diren zutabeak + setting_emails_footer: Eposten oina + setting_protocol: Protokoloa + setting_per_page_options: Orriko objektuen aukerak + setting_user_format: Erabiltzaileak erakusteko formatua + setting_activity_days_default: Proiektuen jardueran erakusteko egunak + setting_display_subprojects_issues: Azpiproiektuen zereginak proiektu nagusian erakutsi defektuz + setting_enabled_scm: Gaitutako IKKak + setting_mail_handler_body_delimiters: "Lerro hauteko baten ondoren epostak moztu" + setting_mail_handler_api_enabled: Sarrerako epostentzako WS gaitu + setting_mail_handler_api_key: API giltza + setting_sequential_project_identifiers: Proiektuen identifikadore sekuentzialak sortu + setting_gravatar_enabled: Erabili Gravatar erabiltzaile ikonoak + setting_gravatar_default: Lehenetsitako Gravatar irudia + setting_diff_max_lines_displayed: Erakutsiko diren diff lerro kopuru maximoa + setting_file_max_size_displayed: Barnean erakuzten diren testu fitxategien tamaina maximoa + setting_repository_log_display_limit: Egunkari fitxategian erakutsiko diren berrikuspen kopuru maximoa. + setting_openid: Baimendu OpenID saio hasiera eta erregistatzea + setting_password_min_length: Pasahitzen luzera minimoa + setting_new_project_user_role_id: Proiektu berriak sortzerakoan kudeatzaile ez diren erabiltzaileei esleitutako rola + setting_default_projects_modules: Proiektu berrientzako defektuz gaitutako moduluak + setting_issue_done_ratio: "Zereginen burututako tasa kalkulatzean erabili:" + setting_issue_done_ratio_issue_field: Zeregin eremua erabili + setting_issue_done_ratio_issue_status: Zeregin egoera erabili + setting_start_of_week: "Egutegiak noiz hasi:" + setting_rest_api_enabled: Gaitu REST web zerbitzua + + permission_add_project: Proiektua sortu + permission_add_subprojects: Azpiproiektuak sortu + permission_edit_project: Proiektua editatu + permission_select_project_modules: Proiektuaren moduluak hautatu + permission_manage_members: Kideak kudeatu + permission_manage_versions: Bertsioak kudeatu + permission_manage_categories: Arazoen kategoriak kudeatu + permission_view_issues: Zereginak ikusi + permission_add_issues: Zereginak gehitu + permission_edit_issues: Zereginak aldatu + permission_manage_issue_relations: Zereginen erlazioak kudeatu + permission_add_issue_notes: Oharrak gehitu + permission_edit_issue_notes: Oharrak aldatu + permission_edit_own_issue_notes: Nork bere oharrak aldatu + permission_move_issues: Zereginak mugitu + permission_delete_issues: Zereginak ezabatu + permission_manage_public_queries: Galdera publikoak kudeatu + permission_save_queries: Galderak gorde + permission_view_gantt: Gantt grafikoa ikusi + permission_view_calendar: Egutegia ikusi + permission_view_issue_watchers: Behatzaileen zerrenda ikusi + permission_add_issue_watchers: Behatzaileak gehitu + permission_delete_issue_watchers: Behatzaileak ezabatu + permission_log_time: Igarotako denbora erregistratu + permission_view_time_entries: Igarotako denbora ikusi + permission_edit_time_entries: Denbora egunkariak editatu + permission_edit_own_time_entries: Nork bere denbora egunkariak editatu + permission_manage_news: Berriak kudeatu + permission_comment_news: Berrien iruzkinak egin + permission_view_documents: Dokumentuak ikusi + permission_manage_files: Fitxategiak kudeatu + permission_view_files: Fitxategiak ikusi + permission_manage_wiki: Wikia kudeatu + permission_rename_wiki_pages: Wiki orriak berrizendatu + permission_delete_wiki_pages: Wiki orriak ezabatu + permission_view_wiki_pages: Wikia ikusi + permission_view_wiki_edits: Wikiaren historia ikusi + permission_edit_wiki_pages: Wiki orriak editatu + permission_delete_wiki_pages_attachments: Eranskinak ezabatu + permission_protect_wiki_pages: Wiki orriak babestu + permission_manage_repository: Biltegiak kudeatu + permission_browse_repository: Biltegia arakatu + permission_view_changesets: Aldaketak ikusi + permission_commit_access: Commit atzipena + permission_manage_boards: Foroak kudeatu + permission_view_messages: Mezuak ikusi + permission_add_messages: Mezuak bidali + permission_edit_messages: Mezuak aldatu + permission_edit_own_messages: Nork bere mezuak aldatu + permission_delete_messages: Mezuak ezabatu + permission_delete_own_messages: Nork bere mezuak ezabatu + + project_module_issue_tracking: Zereginen jarraipena + project_module_time_tracking: Denbora jarraipena + project_module_news: Berriak + project_module_documents: Dokumentuak + project_module_files: Fitxategiak + project_module_wiki: Wiki + project_module_repository: Biltegia + project_module_boards: Foroak + + label_user: Erabiltzailea + label_user_plural: Erabiltzaileak + label_user_new: Erabiltzaile berria + label_user_anonymous: Ezezaguna + label_project: Proiektua + label_project_new: Proiektu berria + label_project_plural: Proiektuak + label_x_projects: + zero: proiekturik ez + one: proiektu bat + other: "%{count} proiektu" + label_project_all: Proiektu guztiak + label_project_latest: Azken proiektuak + label_issue: Zeregina + label_issue_new: Zeregin berria + label_issue_plural: Zereginak + label_issue_view_all: Zeregin guztiak ikusi + label_issues_by: "Zereginak honengatik: %{value}" + label_issue_added: Zeregina gehituta + label_issue_updated: Zeregina eguneratuta + label_document: Dokumentua + label_document_new: Dokumentu berria + label_document_plural: Dokumentuak + label_document_added: Dokumentua gehituta + label_role: Rola + label_role_plural: Rolak + label_role_new: Rol berria + label_role_and_permissions: Rolak eta baimenak + label_member: Kidea + label_member_new: Kide berria + label_member_plural: Kideak + label_tracker: Aztarnaria + label_tracker_plural: Aztarnariak + label_tracker_new: Aztarnari berria + label_workflow: Lan-fluxua + label_issue_status: Zeregin egoera + label_issue_status_plural: Zeregin egoerak + label_issue_status_new: Egoera berria + label_issue_category: Zeregin kategoria + label_issue_category_plural: Zeregin kategoriak + label_issue_category_new: Kategoria berria + label_custom_field: Eremu pertsonalizatua + label_custom_field_plural: Eremu pertsonalizatuak + label_custom_field_new: Eremu pertsonalizatu berria + label_enumerations: Enumerazioak + label_enumeration_new: Balio berria + label_information: Informazioa + label_information_plural: Informazioa + label_please_login: Saioa hasi mesedez + label_register: Erregistratu + label_login_with_open_id_option: edo OpenID-rekin saioa hasi + label_password_lost: Pasahitza galduta + label_home: Hasiera + label_my_page: Nire orria + label_my_account: Nire kontua + label_my_projects: Nire proiektuak + label_administration: Kudeaketa + label_login: Saioa hasi + label_logout: Saioa bukatu + label_help: Laguntza + label_reported_issues: Berri emandako zereginak + label_assigned_to_me_issues: Niri esleitutako arazoak + label_last_login: Azken konexioa + label_registered_on: Noiz erregistratuta + label_activity: Jarduerak + label_overall_activity: Jarduera guztiak + label_user_activity: "%{value}-(r)en jarduerak" + label_new: Berria + label_logged_as: "Sartutako erabiltzailea:" + label_environment: Ingurune + label_authentication: Autentikazioa + label_auth_source: Autentikazio modua + label_auth_source_new: Autentikazio modu berria + label_auth_source_plural: Autentikazio moduak + label_subproject_plural: Azpiproiektuak + label_subproject_new: Azpiproiektu berria + label_and_its_subprojects: "%{value} eta bere azpiproiektuak" + label_min_max_length: Luzera min - max + label_list: Zerrenda + label_date: Data + label_integer: Osokoa + label_float: Koma higikorrekoa + label_boolean: Boolearra + label_string: Testua + label_text: Testu luzea + label_attribute: Atributua + label_attribute_plural: Atributuak + label_no_data: Ez dago erakusteko daturik + label_change_status: Egoera aldatu + label_history: Historikoa + label_attachment: Fitxategia + label_attachment_new: Fitxategi berria + label_attachment_delete: Fitxategia ezabatu + label_attachment_plural: Fitxategiak + label_file_added: Fitxategia gehituta + label_report: Berri ematea + label_report_plural: Berri emateak + label_news: Berria + label_news_new: Berria gehitu + label_news_plural: Berriak + label_news_latest: Azken berriak + label_news_view_all: Berri guztiak ikusi + label_news_added: Berria gehituta + label_settings: Ezarpenak + label_overview: Gainbegirada + label_version: Bertsioa + label_version_new: Bertsio berria + label_version_plural: Bertsioak + label_close_versions: Burututako bertsioak itxi + label_confirmation: Baieztapena + label_export_to: 'Eskuragarri baita:' + label_read: Irakurri... + label_public_projects: Proiektu publikoak + label_open_issues: irekita + label_open_issues_plural: irekiak + label_closed_issues: itxita + label_closed_issues_plural: itxiak + label_x_open_issues_abbr_on_total: + zero: 0 irekita / %{total} + one: 1 irekita / %{total} + other: "%{count} irekiak / %{total}" + label_x_open_issues_abbr: + zero: 0 irekita + one: 1 irekita + other: "%{count} irekiak" + label_x_closed_issues_abbr: + zero: 0 itxita + one: 1 itxita + other: "%{count} itxiak" + label_total: Guztira + label_permissions: Baimenak + label_current_status: Uneko egoera + label_new_statuses_allowed: Baimendutako egoera berriak + label_all: guztiak + label_none: ezer + label_nobody: inor + label_next: Hurrengoa + label_previous: Aurrekoak + label_used_by: Erabilita + label_details: Xehetasunak + label_add_note: Oharra gehitu + label_per_page: Orriko + label_calendar: Egutegia + label_months_from: hilabete noiztik + label_gantt: Gantt + label_internal: Barnekoa + label_last_changes: "azken %{count} aldaketak" + label_change_view_all: Aldaketa guztiak ikusi + label_personalize_page: Orri hau pertsonalizatu + label_comment: Iruzkin + label_comment_plural: Iruzkinak + label_x_comments: + zero: iruzkinik ez + one: iruzkin 1 + other: "%{count} iruzkin" + label_comment_add: Iruzkina gehitu + label_comment_added: Iruzkina gehituta + label_comment_delete: Iruzkinak ezabatu + label_query: Galdera pertsonalizatua + label_query_plural: Pertsonalizatutako galderak + label_query_new: Galdera berria + label_filter_add: Iragazkia gehitu + label_filter_plural: Iragazkiak + label_equals: da + label_not_equals: ez da + label_in_less_than: baino gutxiagotan + label_in_more_than: baino gehiagotan + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: hauetan + label_today: gaur + label_all_time: denbora guztia + label_yesterday: atzo + label_this_week: aste honetan + label_last_week: pasadan astean + label_last_n_days: "azken %{count} egunetan" + label_this_month: hilabete hau + label_last_month: pasadan hilabetea + label_this_year: urte hau + label_date_range: Data tartea + label_less_than_ago: egun hauek baino gutxiago + label_more_than_ago: egun hauek baino gehiago + label_ago: orain dela + label_contains: dauka + label_not_contains: ez dauka + label_day_plural: egun + label_repository: Biltegia + label_repository_plural: Biltegiak + label_browse: Arakatu + label_branch: Adarra + label_tag: Etiketa + label_revision: Berrikuspena + label_revision_plural: Berrikuspenak + label_revision_id: "%{value} berrikuspen" + label_associated_revisions: Elkartutako berrikuspenak + label_added: gehituta + label_modified: aldatuta + label_copied: kopiatuta + label_renamed: berrizendatuta + label_deleted: ezabatuta + label_latest_revision: Azken berrikuspena + label_latest_revision_plural: Azken berrikuspenak + label_view_revisions: Berrikuspenak ikusi + label_view_all_revisions: Berrikuspen guztiak ikusi + label_max_size: Tamaina maximoa + label_sort_highest: Goraino mugitu + label_sort_higher: Gora mugitu + label_sort_lower: Behera mugitu + label_sort_lowest: Beheraino mugitu + label_roadmap: Ibilbide-mapa + label_roadmap_due_in: "Epea: %{value}" + label_roadmap_overdue: "%{value} berandu" + label_roadmap_no_issues: Ez dago zereginik bertsio honetan + label_search: Bilatu + label_result_plural: Emaitzak + label_all_words: hitz guztiak + label_wiki: Wikia + label_wiki_edit: Wiki edizioa + label_wiki_edit_plural: Wiki edizioak + label_wiki_page: Wiki orria + label_wiki_page_plural: Wiki orriak + label_index_by_title: Izenburuaren araberako indizea + label_index_by_date: Dataren araberako indizea + label_current_version: Uneko bertsioa + label_preview: Aurreikusi + label_feed_plural: Jarioak + label_changes_details: Aldaketa guztien xehetasunak + label_issue_tracking: Zeregin jarraipena + label_spent_time: Igarotako denbora + label_f_hour: "ordu %{value}" + label_f_hour_plural: "%{value} ordu" + label_time_tracking: Denbora jarraipena + label_change_plural: Aldaketak + label_statistics: Estatistikak + label_commits_per_month: Commit-ak hilabeteka + label_commits_per_author: Commit-ak egileka + label_view_diff: Ezberdintasunak ikusi + label_diff_inline: barnean + label_diff_side_by_side: aldez alde + label_options: Aukerak + label_copy_workflow_from: Kopiatu workflow-a hemendik + label_permissions_report: Baimenen txostena + label_watched_issues: Behatutako zereginak + label_related_issues: Erlazionatutako zereginak + label_applied_status: Aplikatutako egoera + label_loading: Kargatzen... + label_relation_new: Erlazio berria + label_relation_delete: Erlazioa ezabatu + label_relates_to: erlazionatuta dago + label_duplicates: bikoizten du + label_duplicated_by: honek bikoiztuta + label_blocks: blokeatzen du + label_blocked_by: honek blokeatuta + label_precedes: aurretik doa + label_follows: jarraitzen du + label_end_to_start: bukaeratik hasierara + label_end_to_end: bukaeratik bukaerara + label_start_to_start: hasieratik hasierhasieratik bukaerara + label_start_to_end: hasieratik bukaerara + label_stay_logged_in: Saioa mantendu + label_disabled: ezgaituta + label_show_completed_versions: Bukatutako bertsioak ikusi + label_me: ni + label_board: Foroa + label_board_new: Foro berria + label_board_plural: Foroak + label_topic_plural: Gaiak + label_message_plural: Mezuak + label_message_last: Azken mezua + label_message_new: Mezu berria + label_message_posted: Mezua gehituta + label_reply_plural: Erantzunak + label_send_information: Erabiltzaileai kontuaren informazioa bidali + label_year: Urtea + label_month: Hilabetea + label_week: Astea + label_date_from: Nork + label_date_to: Nori + label_language_based: Erabiltzailearen hizkuntzaren arabera + label_sort_by: "Ordenazioa: %{value}" + label_send_test_email: Frogako mezua bidali + label_feeds_access_key: Atom atzipen giltza + label_missing_feeds_access_key: Atom atzipen giltza falta da + label_feeds_access_key_created_on: "Atom atzipen giltza orain dela %{value} sortuta" + label_module_plural: Moduluak + label_added_time_by: "%{author}, orain dela %{age} gehituta" + label_updated_time_by: "%{author}, orain dela %{age} eguneratuta" + label_updated_time: "Orain dela %{value} eguneratuta" + label_jump_to_a_project: Joan proiektura... + label_file_plural: Fitxategiak + label_changeset_plural: Aldaketak + label_default_columns: Lehenetsitako zutabeak + label_no_change_option: (Aldaketarik ez) + label_bulk_edit_selected_issues: Hautatutako zereginak batera editatu + label_theme: Itxura + label_default: Lehenetsia + label_search_titles_only: Izenburuetan bakarrik bilatu + label_user_mail_option_all: "Nire proiektu guztietako gertakari guztientzat" + label_user_mail_option_selected: "Hautatutako proiektuetako edozein gertakarientzat..." + label_user_mail_no_self_notified: "Ez dut nik egiten ditudan aldeketen jakinarazpenik jaso nahi" + label_registration_activation_by_email: kontuak epostaz gaitu + label_registration_manual_activation: kontuak eskuz gaitu + label_registration_automatic_activation: kontuak automatikoki gaitu + label_display_per_page: "Orriko: %{value}" + label_age: Adina + label_change_properties: Propietateak aldatu + label_general: Orokorra + label_more: Gehiago + label_scm: IKK + label_plugins: Pluginak + label_ldap_authentication: LDAP autentikazioa + label_downloads_abbr: Desk. + label_optional_description: Aukerako deskribapena + label_add_another_file: Beste fitxategia gehitu + label_preferences: Hobespenak + label_chronological_order: Orden kronologikoan + label_reverse_chronological_order: Alderantzizko orden kronologikoan + label_planning: Planifikazioa + label_incoming_emails: Sarrerako epostak + label_generate_key: Giltza sortu + label_issue_watchers: Behatzaileak + label_example: Adibidea + label_display: Bistaratzea + label_sort: Ordenatu + label_ascending: Gorantz + label_descending: Beherantz + label_date_from_to: "%{start}-tik %{end}-ra" + label_wiki_content_added: Wiki orria gehituta + label_wiki_content_updated: Wiki orria eguneratuta + label_group: Taldea + label_group_plural: Taldeak + label_group_new: Talde berria + label_time_entry_plural: Igarotako denbora + label_version_sharing_none: Ez partekatuta + label_version_sharing_descendants: Azpiproiektuekin + label_version_sharing_hierarchy: Proiektu Hierarkiarekin + label_version_sharing_tree: Proiektu zuhaitzarekin + label_version_sharing_system: Proiektu guztiekin + label_update_issue_done_ratios: Zereginen burututako erlazioa eguneratu + label_copy_source: Iturburua + label_copy_target: Helburua + label_copy_same_as_target: Helburuaren berdina + label_display_used_statuses_only: Aztarnari honetan erabiltzen diren egoerak bakarrik erakutsi + label_api_access_key: API atzipen giltza + label_missing_api_access_key: API atzipen giltza falta da + label_api_access_key_created_on: "API atzipen giltza sortuta orain dela %{value}" + + button_login: Saioa hasi + button_submit: Bidali + button_save: Gorde + button_check_all: Guztiak markatu + button_uncheck_all: Guztiak desmarkatu + button_delete: Ezabatu + button_create: Sortu + button_create_and_continue: Sortu eta jarraitu + button_test: Frogatu + button_edit: Editatu + button_add: Gehitu + button_change: Aldatu + button_apply: Aplikatu + button_clear: Garbitu + button_lock: Blokeatu + button_unlock: Desblokeatu + button_download: Deskargatu + button_list: Zerrenda + button_view: Ikusi + button_move: Mugitu + button_move_and_follow: Mugitu eta jarraitu + button_back: Atzera + button_cancel: Ezeztatu + button_activate: Gahitu + button_sort: Ordenatu + button_log_time: Denbora erregistratu + button_rollback: Itzuli bertsio honetara + button_watch: Behatu + button_unwatch: Behatzen utzi + button_reply: Erantzun + button_archive: Artxibatu + button_unarchive: Desartxibatu + button_reset: Berrezarri + button_rename: Berrizendatu + button_change_password: Pasahitza aldatu + button_copy: Kopiatu + button_copy_and_follow: Kopiatu eta jarraitu + button_annotate: Anotatu + button_update: Eguneratu + button_configure: Konfiguratu + button_quote: Aipatu + button_duplicate: Bikoiztu + button_show: Ikusi + + status_active: gaituta + status_registered: izena emanda + status_locked: blokeatuta + + version_status_open: irekita + version_status_locked: blokeatuta + version_status_closed: itxita + + field_active: Gaituta + + text_select_mail_notifications: Jakinarazpenak zein ekintzetarako bidaliko diren hautatu. + text_regexp_info: adib. ^[A-Z0-9]+$ + text_min_max_length_info: 0k mugarik gabe esan nahi du + text_project_destroy_confirmation: Ziur zaude proiektu hau eta erlazionatutako datu guztiak ezabatu nahi dituzula? + text_subprojects_destroy_warning: "%{value} azpiproiektuak ere ezabatuko dira." + text_workflow_edit: Hautatu rola eta aztarnaria workflow-a editatzeko + text_are_you_sure: Ziur zaude? + text_journal_changed: "%{label} %{old}-(e)tik %{new}-(e)ra aldatuta" + text_journal_set_to: "%{label}-k %{value} balioa hartu du" + text_journal_deleted: "%{label} ezabatuta (%{old})" + text_journal_added: "%{label} %{value} gehituta" + text_tip_issue_begin_day: gaur hasten diren zereginak + text_tip_issue_end_day: gaur bukatzen diren zereginak + text_tip_issue_begin_end_day: gaur hasi eta bukatzen diren zereginak + text_caracters_maximum: "%{count} karaktere gehienez." + text_caracters_minimum: "Gutxienez %{count} karaktereetako luzerakoa izan behar du." + text_length_between: "Luzera %{min} eta %{max} karaktereen artekoa." + text_tracker_no_workflow: Ez da workflow-rik definitu aztarnari honentzako + text_unallowed_characters: Debekatutako karaktereak + text_comma_separated: Balio anitz izan daitezke (komaz banatuta). + text_line_separated: Balio anitz izan daitezke (balio bakoitza lerro batean). + text_issues_ref_in_commit_messages: Commit-en mezuetan zereginak erlazionatu eta konpontzen + text_issue_added: "%{id} zeregina %{author}-(e)k jakinarazi du." + text_issue_updated: "%{id} zeregina %{author}-(e)k eguneratu du." + text_wiki_destroy_confirmation: Ziur zaude wiki hau eta bere eduki guztiak ezabatu nahi dituzula? + text_issue_category_destroy_question: "Zeregin batzuk (%{count}) kategoria honetara esleituta daude. Zer egin nahi duzu?" + text_issue_category_destroy_assignments: Kategoria esleipenak kendu + text_issue_category_reassign_to: Zereginak kategoria honetara esleitu + text_user_mail_option: "Hautatu gabeko proiektuetan, behatzen edo parte hartzen duzun gauzei buruzko jakinarazpenak jasoko dituzu (adib. zu egile zaren edo esleituta dituzun zereginak)." + text_no_configuration_data: "Rolak, aztarnariak, zeregin egoerak eta workflow-ak ez dira oraindik konfiguratu.\nOso gomendagarria de lehenetsitako kkonfigurazioa kargatzea. Kargatu eta gero aldatu ahalko duzu." + text_load_default_configuration: Lehenetsitako konfigurazioa kargatu + text_status_changed_by_changeset: "%{value} aldaketan aplikatuta." + text_issues_destroy_confirmation: 'Ziur zaude hautatutako zeregina(k) ezabatu nahi dituzula?' + text_select_project_modules: 'Hautatu proiektu honetan gaitu behar diren moduluak:' + text_default_administrator_account_changed: Lehenetsitako kudeatzaile kontua aldatuta + text_file_repository_writable: Eranskinen direktorioan idatz daiteke + text_plugin_assets_writable: Pluginen baliabideen direktorioan idatz daiteke + text_rmagick_available: RMagick eskuragarri (aukerazkoa) + text_destroy_time_entries_question: "%{hours} orduei buruz berri eman zen zuk ezabatzera zoazen zereginean. Zer egin nahi duzu?" + text_destroy_time_entries: Ezabatu berri emandako orduak + text_assign_time_entries_to_project: Berri emandako orduak proiektura esleitu + text_reassign_time_entries: 'Berri emandako orduak zeregin honetara esleitu:' + text_user_wrote: "%{value}-(e)k idatzi zuen:" + text_enumeration_destroy_question: "%{count} objetu balio honetara esleituta daude." + text_enumeration_category_reassign_to: 'Beste balio honetara esleitu:' + text_email_delivery_not_configured: "Eposta bidalketa ez dago konfiguratuta eta jakinarazpenak ezgaituta daude.\nKonfiguratu zure SMTP zerbitzaria config/configuration.yml-n eta aplikazioa berrabiarazi hauek gaitzeko." + text_repository_usernames_mapping: "Hautatu edo eguneratu Redmineko erabiltzailea biltegiko egunkarietan topatzen diren erabiltzaile izenekin erlazionatzeko.\nRedmine-n eta biltegian erabiltzaile izen edo eposta berdina duten erabiltzaileak automatikoki erlazionatzen dira." + text_diff_truncated: '... Diff hau moztua izan da erakus daitekeen tamaina maximoa gainditu duelako.' + text_custom_field_possible_values_info: 'Lerro bat balio bakoitzeko' + text_wiki_page_destroy_question: "Orri honek %{descendants} orri seme eta ondorengo ditu. Zer egin nahi duzu?" + text_wiki_page_nullify_children: "Orri semeak erro orri moduan mantendu" + text_wiki_page_destroy_children: "Orri semeak eta beraien ondorengo guztiak ezabatu" + text_wiki_page_reassign_children: "Orri semeak orri guraso honetara esleitu" + text_own_membership_delete_confirmation: "Zure baimen batzuk (edo guztiak) kentzera zoaz eta baliteke horren ondoren proiektu hau ezin editatzea.\n Ziur zaude jarraitu nahi duzula?" + + default_role_manager: Kudeatzailea + default_role_developer: Garatzailea + default_role_reporter: Berriemailea + default_tracker_bug: Errorea + default_tracker_feature: Eginbidea + default_tracker_support: Laguntza + default_issue_status_new: Berria + default_issue_status_in_progress: Lanean + default_issue_status_resolved: Ebatzita + default_issue_status_feedback: Berrelikadura + default_issue_status_closed: Itxita + default_issue_status_rejected: Baztertua + default_doc_category_user: Erabiltzaile dokumentazioa + default_doc_category_tech: Dokumentazio teknikoa + default_priority_low: Baxua + default_priority_normal: Normala + default_priority_high: Altua + default_priority_urgent: Larria + default_priority_immediate: Berehalakoa + default_activity_design: Diseinua + default_activity_development: Garapena + + enumeration_issue_priorities: Zeregin lehentasunak + enumeration_doc_categories: Dokumentu kategoriak + enumeration_activities: Jarduerak (denbora kontrola)) + enumeration_system_activity: Sistemako Jarduera + label_board_sticky: Itsaskorra + label_board_locked: Blokeatuta + permission_export_wiki_pages: Wiki orriak esportatu + setting_cache_formatted_text: Formatudun testua katxeatu + permission_manage_project_activities: Proiektuaren jarduerak kudeatu + error_unable_delete_issue_status: Ezine da zereginaren egoera ezabatu + label_profile: Profila + permission_manage_subtasks: Azpiatazak kudeatu + field_parent_issue: Zeregin gurasoa + label_subtask_plural: Azpiatazak + label_project_copy_notifications: Proiektua kopiatzen den bitartean eposta jakinarazpenak bidali + error_can_not_delete_custom_field: Ezin da eremu pertsonalizatua ezabatu + error_unable_to_connect: Ezin da konektatu (%{value}) + error_can_not_remove_role: Rol hau erabiltzen hari da eta ezin da ezabatu. + error_can_not_delete_tracker: Aztarnari honek zereginak ditu eta ezin da ezabatu. + field_principal: Ekintzaile + label_my_page_block: "Nire orriko blokea" + notice_failed_to_save_members: "Kidea(k) gordetzean errorea: %{errors}." + text_zoom_out: Zooma txikiagotu + text_zoom_in: Zooma handiagotu + notice_unable_delete_time_entry: "Ezin da hautatutako denbora erregistroa ezabatu." + label_overall_spent_time: Igarotako denbora guztira + field_time_entries: "Denbora erregistratu" + project_module_gantt: Gantt + project_module_calendar: Egutegia + button_edit_associated_wikipage: "Esleitutako wiki orria editatu: %{page_title}" + field_text: Testu eremua + label_user_mail_option_only_owner: "Jabea naizen gauzetarako barrarik" + setting_default_notification_option: "Lehenetsitako ohartarazpen aukera" + label_user_mail_option_only_my_events: "Behatzen ditudan edo partaide naizen gauzetarako bakarrik" + label_user_mail_option_only_assigned: "Niri esleitutako gauzentzat bakarrik" + label_user_mail_option_none: "Gertakaririk ez" + field_member_of_group: "Esleituta duenaren taldea" + field_assigned_to_role: "Esleituta duenaren rola" + notice_not_authorized_archived_project: "Atzitu nahi duzun proiektua artxibatua izan da." + label_principal_search: "Bilatu erabiltzaile edo taldea:" + label_user_search: "Erabiltzailea bilatu:" + field_visible: Ikusgai + setting_emails_header: "Eposten goiburua" + setting_commit_logtime_activity_id: "Erregistratutako denboraren jarduera" + text_time_logged_by_changeset: "%{value} aldaketan egindakoa." + setting_commit_logtime_enabled: "Erregistrutako denbora gaitu" + notice_gantt_chart_truncated: Grafikoa moztu da bistara daitekeen elementuen kopuru maximoa gainditu delako (%{max}) + setting_gantt_items_limit: "Gantt grafikoan bistara daitekeen elementu kopuru maximoa" + field_warn_on_leaving_unsaved: Gorde gabeko testua duen orri batetik ateratzen naizenean ohartarazi + text_warn_on_leaving_unsaved: Uneko orritik joaten bazara gorde gabeko testua galduko da. + label_my_queries: Nire galdera pertsonalizatuak + text_journal_changed_no_detail: "%{label} eguneratuta" + label_news_comment_added: Berri batera iruzkina gehituta + button_expand_all: Guztia zabaldu + button_collapse_all: Guztia tolestu + label_additional_workflow_transitions_for_assignee: Erabiltzaileak esleitua duenean baimendutako transtsizio gehigarriak + label_additional_workflow_transitions_for_author: Erabiltzailea egilea denean baimendutako transtsizio gehigarriak + label_bulk_edit_selected_time_entries: Hautatutako denbora egunkariak batera editatu + text_time_entries_destroy_confirmation: Ziur zaude hautatutako denbora egunkariak ezabatu nahi dituzula? + label_role_anonymous: Ezezaguna + label_role_non_member: Ez kidea + label_issue_note_added: Oharra gehituta + label_issue_status_updated: Egoera eguneratuta + label_issue_priority_updated: Lehentasuna eguneratuta + label_issues_visibility_own: Erabiltzaileak sortu edo esleituta dituen zereginak + field_issues_visibility: Zeregin ikusgarritasuna + label_issues_visibility_all: Zeregin guztiak + permission_set_own_issues_private: Nork bere zereginak publiko edo pribatu jarri + field_is_private: Pribatu + permission_set_issues_private: Zereginak publiko edo pribatu jarri + label_issues_visibility_public: Pribatu ez diren zeregin guztiak + text_issues_destroy_descendants_confirmation: Honek %{count} azpiataza ezabatuko ditu baita ere. + field_commit_logs_encoding: Commit-en egunkarien kodetzea + field_scm_path_encoding: Bidearen kodeketa + text_scm_path_encoding_note: "Lehentsita: UTF-8" + field_path_to_repository: Biltegirako bidea + field_root_directory: Erro direktorioa + field_cvs_module: Modulua + field_cvsroot: CVSROOT + text_mercurial_repository_note: Biltegi locala (adib. /hgrepo, c:\hgrepo) + text_scm_command: Komandoa + text_scm_command_version: Bertsioa + 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 zeregina + one: 1 zeregina + other: "%{count} zereginak" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: guztiak + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Azpiproiektuekin + label_cross_project_tree: Proiektu zuhaitzarekin + label_cross_project_hierarchy: Proiektu Hierarkiarekin + label_cross_project_system: Proiektu guztiekin + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Guztira + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/13/132a841db337d7fa641930666ad83dea5d0196ed.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/13/132a841db337d7fa641930666ad83dea5d0196ed.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,115 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 Tracker < ActiveRecord::Base + + CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze + # Fields that can be disabled + # Other (future) fields should be appended, not inserted! + CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze + + before_destroy :check_integrity + has_many :issues + has_many :workflow_rules, :dependent => :delete_all do + def copy(source_tracker) + WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) + end + end + + has_and_belongs_to_many :projects + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' + acts_as_list + + attr_protected :fields_bits + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + + scope :sorted, lambda { order("#{table_name}.position ASC") } + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + def to_s; name end + + def <=>(tracker) + position <=> tracker.position + end + + # Returns an array of IssueStatus that are used + # in the tracker's workflows + def issue_statuses + if @issue_statuses + return @issue_statuses + elsif new_record? + return [] + end + + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). + flatten. + uniq + + @issue_statuses = IssueStatus.find_all_by_id(ids).sort + end + + def disabled_core_fields + i = -1 + @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0} + end + + def core_fields + CORE_FIELDS - disabled_core_fields + end + + def core_fields=(fields) + raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array) + + bits = 0 + CORE_FIELDS.each_with_index do |field, i| + unless fields.include?(field) + bits |= 2 ** i + end + end + self.fields_bits = bits + @disabled_core_fields = nil + core_fields + end + + # Returns the fields that are disabled for all the given trackers + def self.disabled_core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:disabled_core_fields).reduce(:&) + else + [] + end + end + + # Returns the fields that are enabled for one tracker at least + def self.core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:core_fields).reduce(:|) + else + CORE_FIELDS.dup + end + end + +private + def check_integrity + raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any? + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/13/135f3b3b4d0bbdb13b1af83c8e387d0dfa750b59.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/13/135f3b3b4d0bbdb13b1af83c8e387d0dfa750b59.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,43 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 ContextMenusHelper + def context_menu_link(name, url, options={}) + options[:class] ||= '' + if options.delete(:selected) + options[:class] << ' icon-checked disabled' + options[:disabled] = true + end + if options.delete(:disabled) + options.delete(:method) + options.delete(:data) + options[:onclick] = 'return false;' + options[:class] << ' disabled' + url = '#' + end + link_to h(name), url, options + end + + def bulk_update_custom_field_context_menu_link(field, text, value) + context_menu_link h(text), + bulk_update_issues_path(:ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back), + :method => :post, + :selected => (@issue && @issue.custom_field_value(field) == value) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/13/1382b1d3f93b8b3247729bbc8f37c6ee649306fe.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/13/1382b1d3f93b8b3247729bbc8f37c6ee649306fe.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1053 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 :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 => :destroy + 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 :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.builtin_role + 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 + + def principals + @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq + end + + def users + @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq + 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. + where(["activity_id = ?", parent_activity.id]). + update_all("activity_id = #{project_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) + @principals = nil + @users = nil + @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) + all.each { |p| p.set_or_update_position_under(p.parent) } + 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)).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. + includes(:project). + where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED) + end + + # Returns a scope of the Versions used by the project + def shared_versions + if new_record? + Version. + includes(:project). + where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED) + else + @shared_versions ||= begin + r = root? ? self : root + Version. + includes(:project). + where("#{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).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 a scope 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. + sorted. + where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" + + " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" + + " WHERE cfp.project_id = ?)", true, id) + 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 + + # Return the enabled module with the given name + # or nil if the module is not enabled for the project + def enabled_module(name) + name = name.to_s + enabled_modules.detect {|m| m.name == name} + end + + # Return true if the module with the given name is enabled + def module_enabled?(name) + enabled_module(name).present? + 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').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.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(: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) + t = TimeEntryActivity.table_name + scope = TimeEntryActivity.where( + "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)", + time_entry_activities.map(&:parent_id), id + ) + unless include_inactive + scope = scope.active + end + scope + 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 + + public + + # 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 d98d22a98252 -r fb9a13467253 .svn/pristine/13/138f43d777efffd6ecf4f9d5f88f24c526eb7287.svn-base --- a/.svn/pristine/13/138f43d777efffd6ecf4f9d5f88f24c526eb7287.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,135 +0,0 @@ -module ActionView - module Helpers - # Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally - # also build your links manually using ActionView::Helpers::AssetHelper#link_to like so: - # - # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %> - # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %> - module PaginationHelper - unless const_defined?(:DEFAULT_OPTIONS) - DEFAULT_OPTIONS = { - :name => :page, - :window_size => 2, - :always_show_anchors => true, - :link_to_current_page => false, - :params => {} - } - end - - # Creates a basic HTML link bar for the given +paginator+. Links will be created - # for the next and/or previous page and for a number of other pages around the current - # pages position. The +html_options+ hash is passed to +link_to+ when the links are created. - # - # ==== Options - # :name:: the routing name for this paginator - # (defaults to +page+) - # :prefix:: prefix for pagination links - # (i.e. Older Pages: 1 2 3 4) - # :suffix:: suffix for pagination links - # (i.e. 1 2 3 4 <- Older Pages) - # :window_size:: the number of pages to show around - # the current page (defaults to 2) - # :always_show_anchors:: whether or not the first and last - # pages should always be shown - # (defaults to +true+) - # :link_to_current_page:: whether or not the current page - # should be linked to (defaults to - # +false+) - # :params:: any additional routing parameters - # for page URLs - # - # ==== Examples - # # We'll assume we have a paginator setup in @person_pages... - # - # pagination_links(@person_pages) - # # => 1 2 3 ... 10 - # - # pagination_links(@person_pages, :link_to_current_page => true) - # # => 1 2 3 ... 10 - # - # pagination_links(@person_pages, :always_show_anchors => false) - # # => 1 2 3 - # - # pagination_links(@person_pages, :window_size => 1) - # # => 1 2 ... 10 - # - # pagination_links(@person_pages, :params => { :viewer => "flash" }) - # # => 1 2 3 ... - # # 10 - def pagination_links(paginator, options={}, html_options={}) - name = options[:name] || DEFAULT_OPTIONS[:name] - params = (options[:params] || DEFAULT_OPTIONS[:params]).clone - - prefix = options[:prefix] || '' - suffix = options[:suffix] || '' - - pagination_links_each(paginator, options, prefix, suffix) do |n| - params[name] = n - link_to(n.to_s, params, html_options) - end - end - - # Iterate through the pages of a given +paginator+, invoking a - # block for each page number that needs to be rendered as a link. - # - # ==== Options - # :window_size:: the number of pages to show around - # the current page (defaults to +2+) - # :always_show_anchors:: whether or not the first and last - # pages should always be shown - # (defaults to +true+) - # :link_to_current_page:: whether or not the current page - # should be linked to (defaults to - # +false+) - # - # ==== Example - # # Turn paginated links into an Ajax call - # pagination_links_each(paginator, page_options) do |link| - # options = { :url => {:action => 'list'}, :update => 'results' } - # html_options = { :href => url_for(:action => 'list') } - # - # link_to_remote(link.to_s, options, html_options) - # end - def pagination_links_each(paginator, options, prefix = nil, suffix = nil) - options = DEFAULT_OPTIONS.merge(options) - link_to_current_page = options[:link_to_current_page] - always_show_anchors = options[:always_show_anchors] - - current_page = paginator.current_page - window_pages = current_page.window(options[:window_size]).pages - return if window_pages.length <= 1 unless link_to_current_page - - first, last = paginator.first, paginator.last - - html = '' - - html << prefix if prefix - - if always_show_anchors and not (wp_first = window_pages[0]).first? - html << yield(first.number) - html << ' ... ' if wp_first.number - first.number > 1 - html << ' ' - end - - window_pages.each do |page| - if current_page == page && !link_to_current_page - html << page.number.to_s - else - html << yield(page.number) - end - html << ' ' - end - - if always_show_anchors and not (wp_last = window_pages[-1]).last? - html << ' ... ' if last.number - wp_last.number > 1 - html << yield(last.number) - end - - html << suffix if suffix - - html - end - - end # PaginationHelper - end # Helpers -end # ActionView diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/13/13a70fe72c228213d71cc1e6f52274fd3abf3062.svn-base --- a/.svn/pristine/13/13a70fe72c228213d71cc1e6f52274fd3abf3062.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Enumeration < ActiveRecord::Base - include Redmine::SubclassFactory - - default_scope :order => "#{Enumeration.table_name}.position ASC" - - belongs_to :project - - acts_as_list :scope => 'type = \'#{type}\'' - acts_as_customizable - acts_as_tree :order => 'position ASC' - - before_destroy :check_integrity - before_save :check_default - - attr_protected :type - - validates_presence_of :name - validates_uniqueness_of :name, :scope => [:type, :project_id] - validates_length_of :name, :maximum => 30 - - scope :shared, where(:project_id => nil) - scope :sorted, order("#{table_name}.position ASC") - scope :active, where(:active => true) - scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} - - def self.default - # Creates a fake default scope so Enumeration.default will check - # it's type. STI subclasses will automatically add their own - # types to the finder. - if self.descends_from_active_record? - where(:is_default => true, :type => 'Enumeration').first - else - # STI classes are - where(:is_default => true).first - end - end - - # Overloaded on concrete classes - def option_name - nil - end - - def check_default - if is_default? && is_default_changed? - Enumeration.update_all({:is_default => false}, {:type => type}) - end - end - - # Overloaded on concrete classes - def objects_count - 0 - end - - def in_use? - self.objects_count != 0 - end - - # Is this enumeration overiding a system level enumeration? - def is_override? - !self.parent.nil? - end - - alias :destroy_without_reassign :destroy - - # Destroy the enumeration - # If a enumeration is specified, objects are reassigned - def destroy(reassign_to = nil) - if reassign_to && reassign_to.is_a?(Enumeration) - self.transfer_relations(reassign_to) - end - destroy_without_reassign - end - - def <=>(enumeration) - position <=> enumeration.position - end - - def to_s; name end - - # Returns the Subclasses of Enumeration. Each Subclass needs to be - # required in development mode. - # - # Note: subclasses is protected in ActiveRecord - def self.get_subclasses - subclasses - end - - # Does the +new+ Hash override the previous Enumeration? - def self.overridding_change?(new, previous) - if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous) - return false - else - return true - end - end - - # Does the +new+ Hash have the same custom values as the previous Enumeration? - def self.same_custom_values?(new, previous) - previous.custom_field_values.each do |custom_value| - if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s] - return false - end - end - - return true - end - - # Are the new and previous fields equal? - def self.same_active_state?(new, previous) - new = (new == "1" ? true : false) - return new == previous - end - -private - def check_integrity - raise "Can't delete enumeration" if self.in_use? - end - -end - -# Force load the subclasses in development mode -require_dependency 'time_entry_activity' -require_dependency 'document_category' -require_dependency 'issue_priority' diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/13/13f1c7e824023a07ff2178b5bb3f5f1ee4de792e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/13/13f1c7e824023a07ff2178b5bb3f5f1ee4de792e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,25 @@ + + +Redmine 500 error + + +

    Internal error

    +

    An error occurred on the page you were trying to access.
    + If you continue to experience problems please contact your Redmine administrator for assistance.

    +

    If you are the Redmine administrator, check your log files for details about the error.

    +

    Back

    + + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/141194219711b0b953dd51beb008d5ce7e421ba0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/141194219711b0b953dd51beb008d5ce7e421ba0.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,211 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * This file is part of DotClear. + * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All + * rights reserved. + * + * DotClear 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. + * + * DotClear 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 DotClear; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ***** END LICENSE BLOCK ***** +*/ + +/* Modified by JP LANG for textile formatting */ + +// strong +jsToolBar.prototype.elements.strong = { + type: 'button', + title: 'Strong', + fn: { + wiki: function() { this.singleTag('*') } + } +} + +// em +jsToolBar.prototype.elements.em = { + type: 'button', + title: 'Italic', + fn: { + wiki: function() { this.singleTag("_") } + } +} + +// ins +jsToolBar.prototype.elements.ins = { + type: 'button', + title: 'Underline', + fn: { + wiki: function() { this.singleTag('+') } + } +} + +// del +jsToolBar.prototype.elements.del = { + type: 'button', + title: 'Deleted', + fn: { + wiki: function() { this.singleTag('-') } + } +} + +// code +jsToolBar.prototype.elements.code = { + type: 'button', + title: 'Code', + fn: { + wiki: function() { this.singleTag('@') } + } +} + +// spacer +jsToolBar.prototype.elements.space1 = {type: 'space'} + +// headings +jsToolBar.prototype.elements.h1 = { + type: 'button', + title: 'Heading 1', + fn: { + wiki: function() { + this.encloseLineSelection('h1. ', '',function(str) { + str = str.replace(/^h\d+\.\s+/, '') + return str; + }); + } + } +} +jsToolBar.prototype.elements.h2 = { + type: 'button', + title: 'Heading 2', + fn: { + wiki: function() { + this.encloseLineSelection('h2. ', '',function(str) { + str = str.replace(/^h\d+\.\s+/, '') + return str; + }); + } + } +} +jsToolBar.prototype.elements.h3 = { + type: 'button', + title: 'Heading 3', + fn: { + wiki: function() { + this.encloseLineSelection('h3. ', '',function(str) { + str = str.replace(/^h\d+\.\s+/, '') + return str; + }); + } + } +} + +// spacer +jsToolBar.prototype.elements.space2 = {type: 'space'} + +// ul +jsToolBar.prototype.elements.ul = { + type: 'button', + title: 'Unordered list', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^)[#-]?\s*/g,"$1* "); + }); + } + } +} + +// ol +jsToolBar.prototype.elements.ol = { + type: 'button', + title: 'Ordered list', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^)[*-]?\s*/g,"$1# "); + }); + } + } +} + +// spacer +jsToolBar.prototype.elements.space3 = {type: 'space'} + +// bq +jsToolBar.prototype.elements.bq = { + type: 'button', + title: 'Quote', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); + }); + } + } +} + +// unbq +jsToolBar.prototype.elements.unbq = { + type: 'button', + title: 'Unquote', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); + }); + } + } +} + +// pre +jsToolBar.prototype.elements.pre = { + type: 'button', + title: 'Preformatted text', + fn: { + wiki: function() { this.encloseLineSelection('
    \n', '\n
    ') } + } +} + +// spacer +jsToolBar.prototype.elements.space4 = {type: 'space'} + +// wiki page +jsToolBar.prototype.elements.link = { + type: 'button', + title: 'Wiki link', + fn: { + wiki: function() { this.encloseSelection("[[", "]]") } + } +} +// image +jsToolBar.prototype.elements.img = { + type: 'button', + title: 'Image', + fn: { + wiki: function() { this.encloseSelection("!", "!") } + } +} + +// spacer +jsToolBar.prototype.elements.space5 = {type: 'space'} +// help +jsToolBar.prototype.elements.help = { + type: 'button', + title: 'Help', + fn: { + wiki: function() { window.open(this.help_link, '', 'resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes') } + } +} diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/1411d2a90c9093fc6ada5ec42e5d250259f58396.svn-base --- a/.svn/pristine/14/1411d2a90c9093fc6ada5ec42e5d250259f58396.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 SettingsController < ApplicationController - layout 'admin' - menu_item :plugins, :only => :plugin - - before_filter :require_admin - - def index - edit - render :action => 'edit' - end - - def edit - @notifiables = Redmine::Notifiable.all - if request.post? && params[:settings] && params[:settings].is_a?(Hash) - settings = (params[:settings] || {}).dup.symbolize_keys - settings.each do |name, value| - # remove blank values in array settings - value.delete_if {|v| v.blank? } if value.is_a?(Array) - Setting[name] = value - end - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'edit', :tab => params[:tab] - else - @options = {} - user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]} - @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]} - @deliveries = ActionMailer::Base.perform_deliveries - - @guessed_host_and_path = request.host_with_port.dup - @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? - - Redmine::Themes.rescan - end - end - - def plugin - @plugin = Redmine::Plugin.find(params[:id]) - if request.post? - Setting.send "plugin_#{@plugin.id}=", params[:settings] - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'plugin', :id => @plugin.id - else - @partial = @plugin.settings[:partial] - @settings = Setting.send "plugin_#{@plugin.id}" - end - rescue Redmine::PluginNotFound - render_404 - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/1417ec7b1e59a18d042b1d9264d06470b6e7c6bf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/1417ec7b1e59a18d042b1d9264d06470b6e7c6bf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,227 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldsControllerTest < ActionController::TestCase + fixtures :custom_fields, :custom_values, :trackers, :users, :projects + + def setup + @request.session[:user_id] = 1 + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_new_without_type_should_render_select_type + get :new + assert_response :success + assert_template 'select_type' + assert_select 'input[name=type]', CustomField.subclasses.size + assert_select 'input[name=type][checked=checked]', 1 + end + + def test_new_should_work_for_each_customized_class_and_format + custom_field_classes.each do |klass| + Redmine::FieldFormat.available_formats.each do |format_name| + get :new, :type => klass.name, :custom_field => {:field_format => format_name} + assert_response :success + assert_template 'new' + assert_kind_of klass, assigns(:custom_field) + assert_equal format_name, assigns(:custom_field).format.name + assert_select 'form#custom_field_form' do + assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' + assert_select 'input[type=hidden][name=type][value=?]', klass.name + end + end + end + end + + def test_new_should_have_string_default_format + get :new, :type => 'IssueCustomField' + assert_response :success + assert_equal 'string', assigns(:custom_field).format.name + end + + def test_new_issue_custom_field + get :new, :type => 'IssueCustomField' + assert_response :success + assert_template 'new' + assert_select 'form#custom_field_form' do + assert_select 'select#custom_field_field_format[name=?]', 'custom_field[field_format]' do + assert_select 'option[value=user]', :text => 'User' + assert_select 'option[value=version]', :text => 'Version' + end + assert_select 'input[type=checkbox][name=?]', 'custom_field[project_ids][]', Project.count + assert_select 'input[type=hidden][name=?]', 'custom_field[project_ids][]', 1 + assert_select 'input[type=hidden][name=type][value=IssueCustomField]' + end + end + + def test_new_time_entry_custom_field_should_not_show_trackers_and_projects + get :new, :type => 'TimeEntryCustomField' + assert_response :success + assert_template 'new' + assert_select 'form#custom_field_form' do + assert_select 'input[name=?]', 'custom_field[tracker_ids][]', 0 + assert_select 'input[name=?]', 'custom_field[project_ids][]', 0 + end + end + + def test_default_value_should_be_an_input_for_string_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'string'} + assert_response :success + assert_select 'input[name=?]', 'custom_field[default_value]' + end + + def test_default_value_should_be_a_textarea_for_text_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'text'} + assert_response :success + assert_select 'textarea[name=?]', 'custom_field[default_value]' + end + + def test_default_value_should_be_a_checkbox_for_bool_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'bool'} + assert_response :success + assert_select 'select[name=?]', 'custom_field[default_value]' do + assert_select 'option', 3 + end + end + + def test_default_value_should_not_be_present_for_user_custom_field + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'user'} + assert_response :success + assert_select '[name=?]', 'custom_field[default_value]', 0 + end + + def test_new_js + get :new, :type => 'IssueCustomField', :custom_field => {:field_format => 'list'}, :format => 'js' + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + + field = assigns(:custom_field) + assert_equal 'list', field.field_format + end + + def test_new_with_invalid_custom_field_class_should_render_select_type + get :new, :type => 'UnknownCustomField' + assert_response :success + assert_template 'select_type' + end + + def test_create_list_custom_field + assert_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", + :custom_field => {:name => "test_post_new_list", + :default_value => "", + :min_length => "0", + :searchable => "0", + :regexp => "", + :is_for_all => "1", + :possible_values => "0.1\n0.2\n", + :max_length => "0", + :is_filter => "0", + :is_required =>"0", + :field_format => "list", + :tracker_ids => ["1", ""]} + end + assert_redirected_to '/custom_fields?tab=IssueCustomField' + field = IssueCustomField.find_by_name('test_post_new_list') + assert_not_nil field + assert_equal ["0.1", "0.2"], field.possible_values + assert_equal 1, field.trackers.size + end + + def test_create_with_project_ids + assert_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", :custom_field => { + :name => "foo", :field_format => "string", :is_for_all => "0", :project_ids => ["1", "3", ""] + } + assert_response 302 + end + field = IssueCustomField.order("id desc").first + assert_equal [1, 3], field.projects.map(&:id).sort + end + + def test_create_with_failure + assert_no_difference 'CustomField.count' do + post :create, :type => "IssueCustomField", :custom_field => {:name => ''} + end + assert_response :success + assert_template 'new' + end + + def test_create_without_type_should_render_select_type + assert_no_difference 'CustomField.count' do + post :create, :custom_field => {:name => ''} + end + assert_response :success + assert_template 'select_type' + end + + def test_edit + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_tag 'input', :attributes => {:name => 'custom_field[name]', :value => 'Database'} + end + + def test_edit_invalid_custom_field_should_render_404 + get :edit, :id => 99 + assert_response 404 + end + + def test_update + put :update, :id => 1, :custom_field => {:name => 'New name'} + assert_redirected_to '/custom_fields?tab=IssueCustomField' + + field = CustomField.find(1) + assert_equal 'New name', field.name + end + + def test_update_with_failure + put :update, :id => 1, :custom_field => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + custom_values_count = CustomValue.where(:custom_field_id => 1).count + assert custom_values_count > 0 + + assert_difference 'CustomField.count', -1 do + assert_difference 'CustomValue.count', - custom_values_count do + delete :destroy, :id => 1 + end + end + + assert_redirected_to '/custom_fields?tab=IssueCustomField' + assert_nil CustomField.find_by_id(1) + assert_nil CustomValue.find_by_custom_field_id(1) + end + + def custom_field_classes + files = Dir.glob(File.join(Rails.root, 'app/models/*_custom_field.rb')).map {|f| File.basename(f).sub(/\.rb$/, '') } + classes = files.map(&:classify).map(&:constantize) + assert classes.size > 0 + classes + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/14189ddfaeae5a33a107c23246aa22c4efcabc20.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/14189ddfaeae5a33a107c23246aa22c4efcabc20.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,169 @@ +--- +users_004: + created_on: 2006-07-19 19:34:07 +02:00 + status: 1 + last_login_on: + language: en + # password = foo + salt: 3126f764c3c5ac61cbfc103f25f934cf + hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b + updated_on: 2006-07-19 19:34:07 +02:00 + admin: false + mail: rhill@somenet.foo + lastname: Hill + firstname: Robert + id: 4 + auth_source_id: + mail_notification: all + login: rhill + type: User +users_001: + created_on: 2006-07-19 19:12:21 +02:00 + status: 1 + last_login_on: 2006-07-19 22:57:52 +02:00 + language: en + # password = admin + salt: 82090c953c4a0000a7db253b0691a6b4 + hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150 + updated_on: 2006-07-19 22:57:52 +02:00 + admin: true + mail: admin@somenet.foo + lastname: Admin + firstname: Redmine + id: 1 + auth_source_id: + mail_notification: all + login: admin + type: User +users_002: + created_on: 2006-07-19 19:32:09 +02:00 + status: 1 + last_login_on: 2006-07-19 22:42:15 +02:00 + language: en + # password = jsmith + salt: 67eb4732624d5a7753dcea7ce0bb7d7d + hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc + updated_on: 2006-07-19 22:42:15 +02:00 + admin: false + mail: jsmith@somenet.foo + lastname: Smith + firstname: John + id: 2 + auth_source_id: + mail_notification: all + login: jsmith + type: User +users_003: + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: en + # password = foo + salt: 7599f9963ec07b5a3b55b354407120c0 + hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: dlopper@somenet.foo + lastname: Lopper + firstname: Dave + id: 3 + auth_source_id: + mail_notification: all + login: dlopper + type: User +users_005: + id: 5 + created_on: 2006-07-19 19:33:19 +02:00 + # Locked + status: 3 + last_login_on: + language: en + hashed_password: 1 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: dlopper2@somenet.foo + lastname: Lopper2 + firstname: Dave2 + auth_source_id: + mail_notification: all + login: dlopper2 + type: User +users_006: + id: 6 + created_on: 2006-07-19 19:33:19 +02:00 + status: 0 + last_login_on: + language: '' + hashed_password: 1 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: '' + lastname: Anonymous + firstname: '' + auth_source_id: + mail_notification: only_my_events + login: '' + type: AnonymousUser +users_007: + # A user who does not belong to any project + id: 7 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: 'en' + # password = foo + salt: 7599f9963ec07b5a3b55b354407120c0 + hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: someone@foo.bar + lastname: One + firstname: Some + auth_source_id: + mail_notification: only_my_events + login: someone + type: User +users_008: + id: 8 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: 'it' + # password = foo + salt: 7599f9963ec07b5a3b55b354407120c0 + hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: miscuser8@foo.bar + lastname: Misc + firstname: User + auth_source_id: + mail_notification: only_my_events + login: miscuser8 + type: User +users_009: + id: 9 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: 'it' + hashed_password: 1 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: miscuser9@foo.bar + lastname: Misc + firstname: User + auth_source_id: + mail_notification: only_my_events + login: miscuser9 + type: User +groups_010: + id: 10 + lastname: A Team + type: Group +groups_011: + id: 11 + lastname: B Team + type: Group + + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/14309d64e537e5e954feb549559e6200d128036a.svn-base --- a/.svn/pristine/14/14309d64e537e5e954feb549559e6200d128036a.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,326 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CustomField < ActiveRecord::Base - include Redmine::SubclassFactory - - has_many :custom_values, :dependent => :delete_all - acts_as_list :scope => 'type = \'#{self.class}\'' - serialize :possible_values - - validates_presence_of :name, :field_format - validates_uniqueness_of :name, :scope => :type - validates_length_of :name, :maximum => 30 - validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats - - validate :validate_custom_field - before_validation :set_searchable - - CUSTOM_FIELDS_TABS = [ - {:name => 'IssueCustomField', :partial => 'custom_fields/index', - :label => :label_issue_plural}, - {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', - :label => :label_spent_time}, - {:name => 'ProjectCustomField', :partial => 'custom_fields/index', - :label => :label_project_plural}, - {:name => 'VersionCustomField', :partial => 'custom_fields/index', - :label => :label_version_plural}, - {:name => 'UserCustomField', :partial => 'custom_fields/index', - :label => :label_user_plural}, - {:name => 'GroupCustomField', :partial => 'custom_fields/index', - :label => :label_group_plural}, - {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', - :label => TimeEntryActivity::OptionName}, - {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', - :label => IssuePriority::OptionName}, - {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', - :label => DocumentCategory::OptionName} - ] - - CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]} - - def field_format=(arg) - # cannot change format of a saved custom field - super if new_record? - end - - def set_searchable - # make sure these fields are not searchable - self.searchable = false if %w(int float date bool).include?(field_format) - # make sure only these fields can have multiple values - self.multiple = false unless %w(list user version).include?(field_format) - true - end - - def validate_custom_field - if self.field_format == "list" - errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? - errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array - end - - if regexp.present? - begin - Regexp.new(regexp) - rescue - errors.add(:regexp, :invalid) - end - end - - if default_value.present? && !valid_field_value?(default_value) - errors.add(:default_value, :invalid) - end - end - - def possible_values_options(obj=nil) - case field_format - when 'user', 'version' - if obj.respond_to?(:project) && obj.project - case field_format - when 'user' - obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]} - when 'version' - obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]} - end - elsif obj.is_a?(Array) - obj.collect {|o| possible_values_options(o)}.reduce(:&) - else - [] - end - when 'bool' - [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']] - else - possible_values || [] - end - end - - def possible_values(obj=nil) - case field_format - when 'user', 'version' - possible_values_options(obj).collect(&:last) - when 'bool' - ['1', '0'] - else - values = super() - if values.is_a?(Array) - values.each do |value| - value.force_encoding('UTF-8') if value.respond_to?(:force_encoding) - end - end - values || [] - end - end - - # Makes possible_values accept a multiline string - def possible_values=(arg) - if arg.is_a?(Array) - super(arg.compact.collect(&:strip).select {|v| !v.blank?}) - else - self.possible_values = arg.to_s.split(/[\n\r]+/) - end - end - - def cast_value(value) - casted = nil - unless value.blank? - case field_format - when 'string', 'text', 'list' - casted = value - when 'date' - casted = begin; value.to_date; rescue; nil end - when 'bool' - casted = (value == '1' ? true : false) - when 'int' - casted = value.to_i - when 'float' - casted = value.to_f - when 'user', 'version' - casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i)) - end - end - casted - end - - def value_from_keyword(keyword, customized) - possible_values_options = possible_values_options(customized) - if possible_values_options.present? - keyword = keyword.to_s.downcase - if v = possible_values_options.detect {|text, id| text.downcase == keyword} - if v.is_a?(Array) - v.last - else - v - end - end - else - keyword - end - end - - # Returns a ORDER BY clause that can used to sort customized - # objects by their value of the custom field. - # Returns nil if the custom field can not be used for sorting. - def order_statement - return nil if multiple? - case field_format - when 'string', 'text', 'list', 'date', 'bool' - # COALESCE is here to make sure that blank and NULL values are sorted equally - "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + - " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + - " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + - " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" - when 'int', 'float' - # Make the database cast values into numeric - # Postgresql will raise an error if a value can not be casted! - # CustomValue validations should ensure that it doesn't occur - "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" + - " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + - " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + - " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)" - when 'user', 'version' - value_class.fields_for_order_statement(value_join_alias) - else - nil - end - end - - # Returns a GROUP BY clause that can used to group by custom value - # Returns nil if the custom field can not be used for grouping. - def group_statement - return nil if multiple? - case field_format - when 'list', 'date', 'bool', 'int' - order_statement - when 'user', 'version' - "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + - " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + - " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + - " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" - else - nil - end - end - - def join_for_order_statement - case field_format - when 'user', 'version' - "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + - " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + - " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + - " AND #{join_alias}.custom_field_id = #{id}" + - " AND #{join_alias}.value <> ''" + - " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + - " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + - " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + - " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + - " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" + - " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id" - else - nil - end - end - - def join_alias - "cf_#{id}" - end - - def value_join_alias - join_alias + "_" + field_format - end - - def <=>(field) - position <=> field.position - end - - # Returns the class that values represent - def value_class - case field_format - when 'user', 'version' - field_format.classify.constantize - else - nil - end - end - - def self.customized_class - self.name =~ /^(.+)CustomField$/ - begin; $1.constantize; rescue nil; end - end - - # to move in project_custom_field - def self.for_all - find(:all, :conditions => ["is_for_all=?", true], :order => 'position') - end - - def type_name - nil - end - - # Returns the error messages for the given value - # or an empty array if value is a valid value for the custom field - def validate_field_value(value) - errs = [] - if value.is_a?(Array) - if !multiple? - errs << ::I18n.t('activerecord.errors.messages.invalid') - end - if is_required? && value.detect(&:present?).nil? - errs << ::I18n.t('activerecord.errors.messages.blank') - end - value.each {|v| errs += validate_field_value_format(v)} - else - if is_required? && value.blank? - errs << ::I18n.t('activerecord.errors.messages.blank') - end - errs += validate_field_value_format(value) - end - errs - end - - # Returns true if value is a valid value for the custom field - def valid_field_value?(value) - validate_field_value(value).empty? - end - - def format_in?(*args) - args.include?(field_format) - end - - protected - - # Returns the error message for the given value regarding its format - def validate_field_value_format(value) - errs = [] - if value.present? - errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp) - errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length - errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length - - # Format specific validations - case field_format - when 'int' - errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/ - when 'float' - begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end - when 'date' - errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end - when 'list' - errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value) - end - end - errs - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/14672d79390f5ddff335388aeceb3a8564ea9087.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/14672d79390f5ddff335388aeceb3a8564ea9087.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,209 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 Role < ActiveRecord::Base + # Custom coder for the permissions attribute that should be an + # array of symbols. Rails 3 uses Psych which can be *unbelievably* + # slow on some platforms (eg. mingw32). + class PermissionsAttributeCoder + def self.load(str) + str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym) + end + + def self.dump(value) + YAML.dump(value) + end + end + + # Built-in roles + BUILTIN_NON_MEMBER = 1 + BUILTIN_ANONYMOUS = 2 + + ISSUES_VISIBILITY_OPTIONS = [ + ['all', :label_issues_visibility_all], + ['default', :label_issues_visibility_public], + ['own', :label_issues_visibility_own] + ] + + scope :sorted, lambda { order("#{table_name}.builtin ASC, #{table_name}.position ASC") } + scope :givable, lambda { order("#{table_name}.position ASC").where(:builtin => 0) } + scope :builtin, lambda { |*args| + compare = (args.first == true ? 'not' : '') + where("#{compare} builtin = 0") + } + + before_destroy :check_deletable + has_many :workflow_rules, :dependent => :delete_all do + def copy(source_role) + WorkflowRule.copy(nil, source_role, nil, proxy_association.owner) + end + end + has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id" + + has_many :member_roles, :dependent => :destroy + has_many :members, :through => :member_roles + acts_as_list + + serialize :permissions, ::Role::PermissionsAttributeCoder + attr_protected :builtin + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_inclusion_of :issues_visibility, + :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first), + :if => lambda {|role| role.respond_to?(:issues_visibility)} + + # Copies attributes from another role, arg can be an id or a Role + def copy_from(arg, options={}) + return unless arg.present? + role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s) + self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions") + self.permissions = role.permissions.dup + self + end + + def permissions=(perms) + perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms + write_attribute(:permissions, perms) + end + + def add_permission!(*perms) + self.permissions = [] unless permissions.is_a?(Array) + + permissions_will_change! + perms.each do |p| + p = p.to_sym + permissions << p unless permissions.include?(p) + end + save! + end + + def remove_permission!(*perms) + return unless permissions.is_a?(Array) + permissions_will_change! + perms.each { |p| permissions.delete(p.to_sym) } + save! + end + + # Returns true if the role has the given permission + def has_permission?(perm) + !permissions.nil? && permissions.include?(perm.to_sym) + end + + def <=>(role) + if role + if builtin == role.builtin + position <=> role.position + else + builtin <=> role.builtin + end + else + -1 + end + end + + def to_s + name + end + + def name + case builtin + when 1; l(:label_role_non_member, :default => read_attribute(:name)) + when 2; l(:label_role_anonymous, :default => read_attribute(:name)) + else; read_attribute(:name) + end + end + + # Return true if the role is a builtin role + def builtin? + self.builtin != 0 + end + + # Return true if the role is the anonymous role + def anonymous? + builtin == 2 + end + + # Return true if the role is a project member role + def member? + !self.builtin? + end + + # Return true if role is allowed to do the specified action + # action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + def allowed_to?(action) + if action.is_a? Hash + allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + else + allowed_permissions.include? action + end + end + + # Return all the permissions that can be given to the role + def setable_permissions + setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions + setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER + setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS + setable_permissions + end + + # Find all the roles that can be given to a project member + def self.find_all_givable + Role.givable.all + end + + # Return the builtin 'non member' role. If the role doesn't exist, + # it will be created on the fly. + def self.non_member + find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member') + end + + # Return the builtin 'anonymous' role. If the role doesn't exist, + # it will be created on the fly. + def self.anonymous + find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous') + end + +private + + def allowed_permissions + @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name} + end + + def allowed_actions + @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten + end + + def check_deletable + raise "Can't delete role" if members.any? + raise "Can't delete builtin role" if builtin? + end + + def self.find_or_create_system_role(builtin, name) + role = where(:builtin => builtin).first + if role.nil? + role = create(:name => name, :position => 0) do |r| + r.builtin = builtin + end + raise "Unable to create the #{name} role." if role.new_record? + end + role + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/146e04b787ce58184275bdd027f7e85c4c9ff45f.svn-base --- a/.svn/pristine/14/146e04b787ce58184275bdd027f7e85c4c9ff45f.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -

    <%=l(:label_information_plural)%>

    - -

    <%= Redmine::Info.versioned_name %>

    - - -<% @checklist.each do |label, result| %> - - - - -<% end %> -
    <%= l(label) %><%= image_tag((result ? 'true.png' : 'exclamation.png'), - :style => "vertical-align:bottom;") %>
    -
    -
    -
    <%= Redmine::Info.environment %>
    -
    - -<% html_title(l(:label_information_plural)) -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/149fe7d9ecdbec895382a22950f9b721ccb3953a.svn-base --- a/.svn/pristine/14/149fe7d9ecdbec895382a22950f9b721ccb3953a.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 EnumerationsController < ApplicationController - layout 'admin' - - before_filter :require_admin, :except => :index - before_filter :require_admin_or_api_request, :only => :index - before_filter :build_new_enumeration, :only => [:new, :create] - before_filter :find_enumeration, :only => [:edit, :update, :destroy] - accept_api_auth :index - - helper :custom_fields - - def index - respond_to do |format| - format.html - format.api { - @klass = Enumeration.get_subclass(params[:type]) - if @klass - @enumerations = @klass.shared.sorted.all - else - render_404 - end - } - end - end - - def new - end - - def create - if request.post? && @enumeration.save - flash[:notice] = l(:notice_successful_create) - redirect_to :action => 'index' - else - render :action => 'new' - end - end - - def edit - end - - def update - if request.put? && @enumeration.update_attributes(params[:enumeration]) - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'index' - else - render :action => 'edit' - end - end - - def destroy - if !@enumeration.in_use? - # No associated objects - @enumeration.destroy - redirect_to :action => 'index' - return - elsif params[:reassign_to_id] - if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id]) - @enumeration.destroy(reassign_to) - redirect_to :action => 'index' - return - end - end - @enumerations = @enumeration.class.all - [@enumeration] - end - - private - - def build_new_enumeration - class_name = params[:enumeration] && params[:enumeration][:type] || params[:type] - @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration]) - if @enumeration.nil? - render_404 - end - end - - def find_enumeration - @enumeration = Enumeration.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/14e3e22e1b23a096d9a03825e5ed303e8453da90.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/14e3e22e1b23a096d9a03825e5ed303e8453da90.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,77 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 UserPreferenceTest < ActiveSupport::TestCase + fixtures :users, :user_preferences + + def test_create + user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") + user.login = "newuser" + user.password, user.password_confirmation = "password", "password" + assert user.save + + assert_kind_of UserPreference, user.pref + assert_kind_of Hash, user.pref.others + assert user.pref.save + end + + def test_update + user = User.find(1) + assert_equal true, user.pref.hide_mail + user.pref['preftest'] = 'value' + assert user.pref.save + + user.reload + assert_equal 'value', user.pref['preftest'] + end + + def test_others_hash + user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") + user.login = "newuser" + user.password, user.password_confirmation = "password", "password" + assert user.save + assert_nil user.preference + up = UserPreference.new(:user => user) + assert_kind_of Hash, up.others + up.others = nil + assert_nil up.others + assert up.save + assert_kind_of Hash, up.others + end + + def test_others_should_be_blank_after_initialization + pref = User.new.pref + assert_equal({}, pref.others) + end + + def test_reading_value_from_nil_others_hash + up = UserPreference.new(:user => User.new) + up.others = nil + assert_nil up.others + assert_nil up[:foo] + end + + def test_writing_value_to_nil_others_hash + up = UserPreference.new(:user => User.new) + up.others = nil + assert_nil up.others + up[:foo] = 'bar' + assert_equal 'bar', up[:foo] + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/14/14fc658e5b9713c5a1ccf4d137bc5d9602c668ab.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/14fc658e5b9713c5a1ccf4d137bc5d9602c668ab.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,105 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueStatus < ActiveRecord::Base + before_destroy :check_integrity + has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id" + acts_as_list + + before_destroy :delete_workflow_rules + after_save :update_default + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true + + scope :sorted, lambda { order("#{table_name}.position ASC") } + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + def update_default + IssueStatus.update_all({:is_default => false}, ['id <> ?', id]) if self.is_default? + end + + # Returns the default status for new issues + def self.default + where(:is_default => true).first + end + + # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+ + def self.update_issue_done_ratios + if Issue.use_status_for_done_ratio? + IssueStatus.where("default_done_ratio >= 0").all.each do |status| + Issue.update_all({:done_ratio => status.default_done_ratio}, {:status_id => status.id}) + end + end + + return Issue.use_status_for_done_ratio? + end + + # Returns an array of all statuses the given role can switch to + # Uses association cache when called more than one time + def new_statuses_allowed_to(roles, tracker, author=false, assignee=false) + if roles && tracker + role_ids = roles.collect(&:id) + transitions = workflows.select do |w| + role_ids.include?(w.role_id) && + w.tracker_id == tracker.id && + ((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee)) + end + transitions.map(&:new_status).compact.sort + else + [] + end + end + + # Same thing as above but uses a database query + # More efficient than the previous method if called just once + def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false) + if roles.present? && tracker + conditions = "(author = :false AND assignee = :false)" + conditions << " OR author = :true" if author + conditions << " OR assignee = :true" if assignee + + workflows. + includes(:new_status). + where(["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})", + {:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false} + ]).all. + map(&:new_status).compact.sort + else + [] + end + end + + def <=>(status) + position <=> status.position + end + + def to_s; name end + + private + + def check_integrity + raise "Can't delete status" if Issue.where(:status_id => id).any? + end + + # Deletes associated workflows + def delete_workflow_rules + WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/154fe7afb2c5b5a902a383d73c013d5510f2e999.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/154fe7afb2c5b5a902a383d73c013d5510f2e999.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,60 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldUserFormatTest < ActiveSupport::TestCase + fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues + + def setup + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user') + end + + def test_possible_values_options_with_no_arguments + assert_equal [], @field.possible_values_options + assert_equal [], @field.possible_values_options(nil) + end + + def test_possible_values_options_with_project_resource + project = Project.find(1) + possible_values_options = @field.possible_values_options(project.issues.first) + assert possible_values_options.any? + assert_equal project.users.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_possible_values_options_with_array + projects = Project.find([1, 2]) + possible_values_options = @field.possible_values_options(projects) + assert possible_values_options.any? + assert_equal (projects.first.users & projects.last.users).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_cast_blank_value + assert_equal nil, @field.cast_value(nil) + assert_equal nil, @field.cast_value("") + end + + def test_cast_valid_value + user = @field.cast_value("2") + assert_kind_of User, user + assert_equal User.find(2), user + end + + def test_cast_invalid_value + assert_equal nil, @field.cast_value("187") + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15507fe94b5bb5521b6c8407d27c46737c998477.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/15507fe94b5bb5521b6c8407d27c46737c998477.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,141 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 GroupsController < ApplicationController + layout 'admin' + + before_filter :require_admin + before_filter :find_group, :except => [:index, :new, :create] + accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user + + helper :custom_fields + + def index + @groups = Group.sorted.all + + respond_to do |format| + format.html + format.api + end + end + + def show + respond_to do |format| + format.html + format.api + end + end + + def new + @group = Group.new + end + + def create + @group = Group.new + @group.safe_attributes = params[:group] + + respond_to do |format| + if @group.save + format.html { + flash[:notice] = l(:notice_successful_create) + redirect_to(params[:continue] ? new_group_path : groups_path) + } + format.api { render :action => 'show', :status => :created, :location => group_url(@group) } + else + format.html { render :action => "new" } + format.api { render_validation_errors(@group) } + end + end + end + + def edit + end + + def update + @group.safe_attributes = params[:group] + + respond_to do |format| + if @group.save + flash[:notice] = l(:notice_successful_update) + format.html { redirect_to(groups_path) } + format.api { render_api_ok } + else + format.html { render :action => "edit" } + format.api { render_validation_errors(@group) } + end + end + end + + def destroy + @group.destroy + + respond_to do |format| + format.html { redirect_to(groups_path) } + format.api { render_api_ok } + end + end + + def add_users + @users = User.where(:id => (params[:user_id] || params[:user_ids])).all + @group.users << @users if request.post? + respond_to do |format| + format.html { redirect_to edit_group_path(@group, :tab => 'users') } + format.js + format.api { render_api_ok } + end + end + + def remove_user + @group.users.delete(User.find(params[:user_id])) if request.delete? + respond_to do |format| + format.html { redirect_to edit_group_path(@group, :tab => 'users') } + format.js + format.api { render_api_ok } + end + end + + def autocomplete_for_user + respond_to do |format| + format.js + end + end + + def edit_membership + @membership = Member.edit_membership(params[:membership_id], params[:membership], @group) + @membership.save if request.post? + respond_to do |format| + format.html { redirect_to edit_group_path(@group, :tab => 'memberships') } + format.js + end + end + + def destroy_membership + Member.find(params[:membership_id]).destroy if request.post? + respond_to do |format| + format.html { redirect_to edit_group_path(@group, :tab => 'memberships') } + format.js + end + end + + private + + def find_group + @group = Group.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/155d97c62e3af9eb00c14de47a9d0bc572afcd07.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/155d97c62e3af9eb00c14de47a9d0bc572afcd07.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,75 @@ +
    +<% if @editable %> +<% if @content.current_version? %> + <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :id => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> + <%= watcher_link(@page, User.current) %> + <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :id => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %> + <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :id => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %> + <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') %> + <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :id => @page.title}, :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del') %> +<% else %> + <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :id => @page.title, :version => @content.version }, :class => 'icon icon-cancel') %> +<% end %> +<% end %> +<%= link_to_if_authorized(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %> +
    + +<%= wiki_page_breadcrumb(@page) %> + +<% unless @content.current_version? %> + <%= title [@page.pretty_title, project_wiki_page_path(@page.project, @page.title, :version => nil)], + [l(:label_history), history_project_wiki_page_path(@page.project, @page.title)], + "#{l(:label_version)} #{@content.version}" %> + +

    + <%= link_to(("\xc2\xab " + l(:label_previous)), + :action => 'show', :id => @page.title, :project_id => @page.project, + :version => @content.previous.version) + " - " if @content.previous %> + <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %> + <%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff', + :id => @page.title, :project_id => @page.project, + :version => @content.version) + ')'.html_safe if @content.previous %> - + <%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show', + :id => @page.title, :project_id => @page.project, + :version => @content.next.version) + " - " if @content.next %> + <%= link_to(l(:label_current_version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => nil) %> +
    + <%= @content.author ? link_to_user(@content.author) : l(:label_user_anonymous) + %>, <%= format_time(@content.updated_on) %>
    + <%=h @content.comments %> +

    +
    +<% end %> + +<%= render(:partial => "wiki/content", :locals => {:content => @content}) %> + +<%= link_to_attachments @page %> + +<% if @editable && authorize_for('wiki', 'add_attachment') %> +
    +

    <%= link_to l(:label_attachment_new), {}, :onclick => "$('#add_attachment_form').show(); return false;", + :id => 'attach_files_link' %>

    +<%= form_tag({:controller => 'wiki', :action => 'add_attachment', + :project_id => @project, :id => @page.title}, + :multipart => true, :id => "add_attachment_form", + :style => "display:none;") do %> +
    +

    <%= render :partial => 'attachments/form' %>

    +
    +<%= submit_tag l(:button_add) %> +<%= link_to l(:button_cancel), {}, :onclick => "$('#add_attachment_form').hide(); return false;" %> +<% end %> +
    +<% end %> + +<% other_formats_links do |f| %> + <%= f.link_to 'PDF', :url => {:id => @page.title, :version => params[:version]} %> + <%= f.link_to 'HTML', :url => {:id => @page.title, :version => params[:version]} %> + <%= f.link_to 'TXT', :url => {:id => @page.title, :version => params[:version]} %> +<% end if User.current.allowed_to?(:export_wiki_pages, @project) %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% html_title @page.pretty_title %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/1562dffbb640b06caf248c7277e18410c01feded.svn-base --- a/.svn/pristine/15/1562dffbb640b06caf248c7277e18410c01feded.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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::CodesetUtilTest < ActiveSupport::TestCase - - def test_to_utf8_by_setting_from_latin1 - with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do - s1 = "Texte encod\xc3\xa9" - s2 = "Texte encod\xe9" - s3 = s2.dup - if s1.respond_to?(:force_encoding) - s1.force_encoding("UTF-8") - s2.force_encoding("ASCII-8BIT") - s3.force_encoding("UTF-8") - end - assert_equal s1, Redmine::CodesetUtil.to_utf8_by_setting(s2) - assert_equal s1, Redmine::CodesetUtil.to_utf8_by_setting(s3) - end - end - - def test_to_utf8_by_setting_from_euc_jp - with_settings :repositories_encodings => 'UTF-8,EUC-JP' do - s1 = "\xe3\x83\xac\xe3\x83\x83\xe3\x83\x89\xe3\x83\x9e\xe3\x82\xa4\xe3\x83\xb3" - s2 = "\xa5\xec\xa5\xc3\xa5\xc9\xa5\xde\xa5\xa4\xa5\xf3" - s3 = s2.dup - if s1.respond_to?(:force_encoding) - s1.force_encoding("UTF-8") - s2.force_encoding("ASCII-8BIT") - s3.force_encoding("UTF-8") - end - assert_equal s1, Redmine::CodesetUtil.to_utf8_by_setting(s2) - assert_equal s1, Redmine::CodesetUtil.to_utf8_by_setting(s3) - end - end - - def test_to_utf8_by_setting_should_be_converted_all_latin1 - with_settings :repositories_encodings => 'ISO-8859-1' do - s1 = "\xc3\x82\xc2\x80" - s2 = "\xC2\x80" - s3 = s2.dup - if s1.respond_to?(:force_encoding) - s1.force_encoding("UTF-8") - s2.force_encoding("ASCII-8BIT") - s3.force_encoding("UTF-8") - end - assert_equal s1, Redmine::CodesetUtil.to_utf8_by_setting(s2) - assert_equal s1, Redmine::CodesetUtil.to_utf8_by_setting(s3) - end - end - - def test_to_utf8_by_setting_blank_string - assert_equal "", Redmine::CodesetUtil.to_utf8_by_setting("") - assert_equal nil, Redmine::CodesetUtil.to_utf8_by_setting(nil) - end - - def test_to_utf8_by_setting_returns_ascii_as_utf8 - s1 = "ASCII" - s2 = s1.dup - if s1.respond_to?(:force_encoding) - s1.force_encoding("UTF-8") - s2.force_encoding("ISO-8859-1") - end - str1 = Redmine::CodesetUtil.to_utf8_by_setting(s1) - str2 = Redmine::CodesetUtil.to_utf8_by_setting(s2) - assert_equal s1, str1 - assert_equal s1, str2 - if s1.respond_to?(:force_encoding) - assert_equal "UTF-8", str1.encoding.to_s - assert_equal "UTF-8", str2.encoding.to_s - end - end - - def test_to_utf8_by_setting_invalid_utf8_sequences_should_be_stripped - with_settings :repositories_encodings => '' do - # s1 = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") - s1 = "Texte encod\xe9 en ISO-8859-1." - s1.force_encoding("ASCII-8BIT") if s1.respond_to?(:force_encoding) - str = Redmine::CodesetUtil.to_utf8_by_setting(s1) - if str.respond_to?(:force_encoding) - assert str.valid_encoding? - assert_equal "UTF-8", str.encoding.to_s - end - assert_equal "Texte encod? en ISO-8859-1.", str - end - end - - def test_to_utf8_by_setting_invalid_utf8_sequences_should_be_stripped_ja_jis - with_settings :repositories_encodings => 'ISO-2022-JP' do - s1 = "test\xb5\xfetest\xb5\xfe" - s1.force_encoding("ASCII-8BIT") if s1.respond_to?(:force_encoding) - str = Redmine::CodesetUtil.to_utf8_by_setting(s1) - if str.respond_to?(:force_encoding) - assert str.valid_encoding? - assert_equal "UTF-8", str.encoding.to_s - end - assert_equal "test??test??", str - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/1563561117c58a8d11c2e6605c5ed92c8394ca39.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/1563561117c58a8d11c2e6605c5ed92c8394ca39.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,70 @@ +Installing gems for testing +=========================== + +Remove your .bundle/config if you've already installed Redmine without +the test dependencies. Then, run `bundle install`. + +Running Tests +============= + +Run `rake --tasks test` to see available tests. +Run `rake test` to run the entire test suite (except the tests for the +Apache perl module Redmine.pm and Capybara tests, see below). + +You can run `ruby test/unit/issue_test.rb` for running a single test case and +`ruby test/unit/issue_test.rb -n test_create` for running a single test. + +Before running tests, you need to configure both development +and test databases. + +Creating test repositories +========================== + +Redmine supports a wide array of different version control systems. +To test the support, a test repository needs to be created for each of those. + +Run `rake --tasks test:scm:setup` for a list of available test-repositories or +run `rake test:scm:setup:all` to set up all of them. The repositories are +unpacked into {redmine_root}/tmp/test. + +If the test repositories are not present, the tests that need them will be +skipped. + +Creating a test ldap database +============================= + +Redmine supports using LDAP for user authentications. To test LDAP +with Redmine, load the LDAP export from test/fixtures/ldap/test-ldap.ldif +into a testing LDAP server. Make sure that the LDAP server can be accessed +at 127.0.0.1 on port 389. + +Setting up the test LDAP server is beyond the scope of this documentation. +The OpenLDAP project provides a simple LDAP implementation that should work +good as a test server. + +If the LDAP is not available, the tests that need it will be skipped. + +Running Redmine.pm tests +======================== + +(work in progress) + +Running the tests for the Redmine.pm perl module needs a bit more setup. +You need an Apache server with mod_perl, mod_dav_svn and Redmine.pm configured. +See: http://www.redmine.org/projects/redmine/wiki/Repositories_access_control_with_apache_mod_dav_svn_and_mod_perl + +You need an empty repository accessible at http://127.0.0.1/svn/ecookbook +Then, you can run the tests with: +`ruby test\extra\redmine_pm\repository_subversion_test.rb` + +If you svn server is not running on localhost, you can use the REDMINE_TEST_DAV_SERVER +environment variable to specify another host. + +Running Capybara tests +====================== + +You need to have PhantomJS WebDriver listening on port 4444: +`phantomjs --webdriver 4444` + +Capybara tests can be run with: +`rake test:ui` diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/158142f819942eaf61a29b3cf45389ea3967c9ba.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/158142f819942eaf61a29b3cf45389ea3967c9ba.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1123 @@ +# Hebrew translation for Redmine +# Initiated by Dotan Nahum (dipidi@gmail.com) +# Jul 2010 - Updated by Orgad Shaneh (orgads@gmail.com) + +he: + direction: rtl + date: + formats: + default: "%d/%m/%Y" + short: "%d/%m" + long: "%d/%m/%Y" + only_day: "%e" + + day_names: [ר×שון, שני, שלישי, רביעי, חמישי, שישי, שבת] + abbr_day_names: ["×'", "ב'", "×’'", "ד'", "×”'", "ו'", "ש'"] + month_names: [~, ינו×ר, פברו×ר, מרץ, ×פריל, מ××™, יוני, יולי, ×וגוסט, ספטמבר, ×וקטובר, נובמבר, דצמבר] + abbr_month_names: [~, ×™×× , פבר, מרץ, ×פר, מ××™, יונ, יול, ×וג, ספט, ×וק, נוב, דצמ] + order: + - :day + - :month + - :year + + time: + formats: + default: "%a %d/%m/%Y %H:%M:%S" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + only_second: "%S" + + datetime: + formats: + default: "%d-%m-%YT%H:%M:%S%Z" + + am: 'am' + pm: 'pm' + + datetime: + distance_in_words: + half_a_minute: 'חצי דקה' + less_than_x_seconds: + zero: 'פחות משניה' + one: 'פחות משניה' + other: 'פחות מ־%{count} שניות' + x_seconds: + one: 'שניה ×חת' + other: '%{count} שניות' + less_than_x_minutes: + zero: 'פחות מדקה ×חת' + 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: "כמעט שנה" + other: "כמעט %{count} שני×" + + number: + format: + precision: 3 + separator: '.' + delimiter: ',' + currency: + format: + unit: 'ש"×—' + precision: 2 + format: '%u %n' + human: + storage_units: + format: "%n %u" + units: + byte: + one: "בייט" + other: "בתי×" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: + sentence_connector: "וג×" + skip_last_comma: true + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "×œ× × ×›×œ×œ ברשימה" + exclusion: "×œ× ×–×ž×™×Ÿ" + invalid: "×œ× ×•×œ×™×“×™" + confirmation: "×œ× ×ª×•×× ×œ×ישור" + accepted: "חייב ב×ישור" + empty: "חייב להכלל" + blank: "חייב להכלל" + too_long: "×רוך מדי (×œ× ×™×•×ª×¨ מ־%{count} תוי×)" + too_short: "קצר מדי (×œ× ×™×•×ª×¨ מ־%{count} תוי×)" + wrong_length: "×œ× ×‘×ורך הנכון (חייב להיות %{count} תוי×)" + taken: "×œ× ×–×ž×™×Ÿ" + not_a_number: "×”×•× ×œ× ×ž×¡×¤×¨" + 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: 'Hebrew (עברית)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-8 + general_pdf_encoding: UTF-8 + 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: ×חוזי התקדמות ×œ× ×•×©× ×¢×•×“×›× ×•. + + 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: 'לשינו סיסמת ×”Ö¾Redmine שלך, לחץ על הקישור הב×:' + mail_subject_register: "הפעלת חשבון %{value}" + mail_body_register: 'להפעלת חשבון ×”Ö¾Redmine שלך, לחץ על הקישור הב×:' + 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: "דף ×”Ö¾wiki â€'%{id}' נוסף" + mail_body_wiki_content_added: דף ×”Ö¾wiki â€'%{id}' נוסף ×¢"×™ %{author}. + mail_subject_wiki_content_updated: "דף ×”Ö¾wiki â€'%{id}' עודכן" + mail_body_wiki_content_updated: דף ×”Ö¾wiki â€'%{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: 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_text: שדה טקסט + + 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: גבול תוכן הזנות + 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_footer: תחתית דו×"ל + setting_protocol: פרוטוקול + setting_per_page_options: ×פשרויות ××•×‘×™×§×˜×™× ×œ×¤×™ דף + setting_user_format: פורמט הצגת ×ž×©×ª×ž×©×™× + setting_activity_days_default: ×™×ž×™× ×”×ž×•×¦×’×™× ×¢×œ פעילות הפרויקט + setting_display_subprojects_issues: הצג נוש××™× ×©×œ ×ª×ª×™Ö¾×¤×¨×•×™×§×˜×™× ×›×‘×¨×™×¨×ª מחדל + setting_enabled_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: ×פשרות התר××” ברירת־מחדל + + 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: פרויקט ×חד + 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: הערה ×חת + 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: הזנות + 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_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_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: מערכת ניהול תצורה + 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: צור מפתח + 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: שלח התר×ות דו×ר במהלך העתקת הפרויקט + + 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_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 בקובץ /etc/redmine/<instance>/configuration.yml והתחל ×ת ×”×פליקציה מחדש ×¢"מ ל×פשר ×ות×.' + 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: פעילות מערכת + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: הכל + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: ×¢× ×¤×¨×•×™×§×˜×™× ×‘× ×™× + label_cross_project_tree: ×¢× ×¢×¥ הפרויקט + label_cross_project_hierarchy: ×¢× ×”×™×¨×¨×›×™×ª ×”×¤×¨×•×™×§×˜×™× + label_cross_project_system: ×¢× ×›×œ ×”×¤×¨×•×™×§×˜×™× + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: סה"×› + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15a8c97e1386ace05f99c6101e9b7756be58a55c.svn-base --- a/.svn/pristine/15/15a8c97e1386ace05f99c6101e9b7756be58a55c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CommentObserver < ActiveRecord::Observer - def after_create(comment) - if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added') - Mailer.news_comment_added(comment).deliver - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15c23dcf913596b86dcad4baa04c5641b5253d49.svn-base --- a/.svn/pristine/15/15c23dcf913596b86dcad4baa04c5641b5253d49.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1122 +0,0 @@ -# Swedish translation for Ruby on Rails -# by Johan Lundström (johanlunds@gmail.com), -# with parts taken from http://github.com/daniel/swe_rails - -sv: - number: - # Used in number_with_delimiter() - # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' - format: - # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) - separator: "," - # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three) - delimiter: "." - # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) - precision: 2 - - # Used in number_to_currency() - currency: - format: - # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) - format: "%n %u" - unit: "kr" - # These three are to override number.format and are optional - # separator: "." - # delimiter: "," - # precision: 2 - - # Used in number_to_percentage() - percentage: - format: - # These three are to override number.format and are optional - # separator: - delimiter: "" - # precision: - - # Used in number_to_precision() - precision: - format: - # These three are to override number.format and are optional - # separator: - delimiter: "" - # precision: - - # Used in number_to_human_size() - human: - format: - # These three are to override number.format and are optional - # separator: - delimiter: "" - precision: 3 - storage_units: - format: "%n %u" - units: - byte: - one: "Byte" - other: "Bytes" - kb: "KB" - mb: "MB" - gb: "GB" - tb: "TB" - - # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() - datetime: - distance_in_words: - half_a_minute: "en halv minut" - less_than_x_seconds: - one: "mindre än en sekund" - other: "mindre än %{count} sekunder" - x_seconds: - one: "en sekund" - other: "%{count} sekunder" - less_than_x_minutes: - one: "mindre än en minut" - other: "mindre än %{count} minuter" - x_minutes: - one: "en minut" - other: "%{count} minuter" - about_x_hours: - one: "ungefär en timme" - other: "ungefär %{count} timmar" - x_hours: - one: "1 timme" - other: "%{count} timmar" - x_days: - one: "en dag" - other: "%{count} dagar" - about_x_months: - one: "ungefär en mÃ¥nad" - other: "ungefär %{count} mÃ¥nader" - x_months: - one: "en mÃ¥nad" - other: "%{count} mÃ¥nader" - about_x_years: - one: "ungefär ett Ã¥r" - other: "ungefär %{count} Ã¥r" - over_x_years: - one: "mer än ett Ã¥r" - other: "mer än %{count} Ã¥r" - almost_x_years: - one: "nästan 1 Ã¥r" - other: "nästan %{count} Ã¥r" - - activerecord: - errors: - template: - header: - one: "Ett fel förhindrade denna %{model} frÃ¥n att sparas" - other: "%{count} fel förhindrade denna %{model} frÃ¥n att sparas" - # The variable :count is also available - body: "Det var problem med följande fält:" - # The values :model, :attribute and :value are always available for interpolation - # The value :count is available when applicable. Can be used for pluralization. - messages: - inclusion: "finns inte i listan" - exclusion: "är reserverat" - invalid: "är ogiltigt" - confirmation: "stämmer inte överens" - accepted : "mÃ¥ste vara accepterad" - empty: "fÃ¥r ej vara tom" - blank: "mÃ¥ste anges" - too_long: "är för lÃ¥ng (maximum är %{count} tecken)" - too_short: "är för kort (minimum är %{count} tecken)" - wrong_length: "har fel längd (ska vara %{count} tecken)" - taken: "har redan tagits" - not_a_number: "är inte ett nummer" - greater_than: "mÃ¥ste vara större än %{count}" - greater_than_or_equal_to: "mÃ¥ste vara större än eller lika med %{count}" - equal_to: "mÃ¥ste vara samma som" - less_than: "mÃ¥ste vara mindre än %{count}" - less_than_or_equal_to: "mÃ¥ste vara mindre än eller lika med %{count}" - odd: "mÃ¥ste vara udda" - even: "mÃ¥ste vara jämnt" - greater_than_start_date: "mÃ¥ste vara senare än startdatumet" - not_same_project: "tillhör inte samma projekt" - circular_dependency: "Denna relation skulle skapa ett cirkulärt beroende" - cant_link_an_issue_with_a_descendant: "Ett ärende kan inte länkas till ett av dess underärenden" - - direction: ltr - 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: "%e %b" - long: "%e %B, %Y" - - day_names: [söndag, mÃ¥ndag, tisdag, onsdag, torsdag, fredag, lördag] - abbr_day_names: [sön, mÃ¥n, tis, ons, tor, fre, lör] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, januari, februari, mars, april, maj, juni, juli, augusti, september, oktober, november, december] - abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, aug, sep, okt, nov, dec] - # Used in date_select and datime_select. - order: - - :day - - :month - - :year - - time: - formats: - default: "%Y-%m-%d %H:%M" - time: "%H:%M" - short: "%d %b %H:%M" - long: "%d %B, %Y %H:%M" - am: "" - pm: "" - -# Used in array.to_sentence. - support: - array: - sentence_connector: "och" - skip_last_comma: true - - actionview_instancetag_blank_option: Var god välj - - general_text_No: 'Nej' - general_text_Yes: 'Ja' - general_text_no: 'nej' - general_text_yes: 'ja' - general_lang_name: 'Svenska' - general_csv_separator: ',' - general_csv_decimal_separator: '.' - general_csv_encoding: ISO-8859-1 - general_pdf_encoding: UTF-8 - general_first_day_of_week: '1' - - notice_account_updated: Kontot har uppdaterats - notice_account_invalid_creditentials: Fel användarnamn eller lösenord - notice_account_password_updated: Lösenordet har uppdaterats - notice_account_wrong_password: Fel lösenord - notice_account_register_done: Kontot har skapats. För att aktivera kontot, klicka pÃ¥ länken i mailet som skickades till dig. - notice_account_unknown_email: Okänd användare. - notice_can_t_change_password: Detta konto använder en extern autentiseringskälla. Det gÃ¥r inte att byta lösenord. - notice_account_lost_email_sent: Ett mail med instruktioner om hur man väljer ett nytt lösenord har skickats till dig. - notice_account_activated: Ditt konto har blivit aktiverat. Du kan nu logga in. - notice_successful_create: Skapades korrekt. - notice_successful_update: Uppdatering lyckades. - notice_successful_delete: Borttagning lyckades. - notice_successful_connection: Uppkoppling lyckades. - notice_file_not_found: Sidan du försökte komma Ã¥t existerar inte eller är borttagen. - notice_locking_conflict: Data har uppdaterats av en annan användare. - notice_not_authorized: Du saknar behörighet att komma Ã¥t den här sidan. - notice_not_authorized_archived_project: Projektet du försöker komma Ã¥t har arkiverats. - notice_email_sent: "Ett mail skickades till %{value}" - notice_email_error: "Ett fel inträffade när mail skickades (%{value})" - notice_feeds_access_key_reseted: Din RSS-nyckel Ã¥terställdes. - notice_api_access_key_reseted: Din API-nyckel Ã¥terställdes. - notice_failed_to_save_issues: "Misslyckades med att spara %{count} ärende(n) pÃ¥ %{total} valda: %{ids}." - notice_failed_to_save_time_entries: "Misslyckades med att spara %{count} tidloggning(ar) pÃ¥ %{total} valda: %{ids}." - notice_failed_to_save_members: "Misslyckades med att spara medlem(mar): %{errors}." - notice_no_issue_selected: "Inget ärende är markerat! Var vänlig, markera de ärenden du vill ändra." - notice_account_pending: "Ditt konto skapades och avvaktar nu administratörens godkännande." - notice_default_data_loaded: Standardkonfiguration inläst. - notice_unable_delete_version: Denna version var inte möjlig att ta bort. - notice_unable_delete_time_entry: Tidloggning kunde inte tas bort. - notice_issue_done_ratios_updated: "% klart uppdaterade." - notice_gantt_chart_truncated: "Schemat förminskades eftersom det överskrider det maximala antalet aktiviteter som fÃ¥r visas (%{max})" - notice_issue_successful_create: Ärende %{id} skapades. - notice_issue_update_conflict: Detta ärende har uppdaterats av en annan användare samtidigt som du redigerade det. - notice_account_deleted: Ditt konto har avslutats permanent. - notice_user_successful_create: "Användare %{id} skapad." - - error_can_t_load_default_data: "Standardkonfiguration gick inte att läsa in: %{value}" - error_scm_not_found: "Inlägg och/eller revision finns inte i detta versionsarkiv." - error_scm_command_failed: "Ett fel inträffade vid försök att nÃ¥ versionsarkivet: %{value}" - error_scm_annotate: "Inlägget existerar inte eller kan inte kommenteras." - error_scm_annotate_big_text_file: Inlägget kan inte annoteras eftersom det överskrider maximal storlek för textfiler. - error_issue_not_found_in_project: 'Ärendet hittades inte eller sÃ¥ tillhör det inte detta projekt' - error_no_tracker_in_project: 'Ingen ärendetyp är associerad med projektet. Vänligen kontrollera projektinställningarna.' - error_no_default_issue_status: 'Ingen status är definierad som standard för nya ärenden. Vänligen kontrollera din konfiguration (GÃ¥ till "Administration -> Ärendestatus").' - error_can_not_delete_custom_field: Kan inte ta bort användardefinerat fält - error_can_not_delete_tracker: "Det finns ärenden av denna typ och den är därför inte möjlig att ta bort." - error_can_not_remove_role: "Denna roll används och den är därför inte möjlig att ta bort." - error_can_not_reopen_issue_on_closed_version: 'Ett ärende tilldelat en stängd version kan inte öppnas pÃ¥ nytt' - error_can_not_archive_project: Detta projekt kan inte arkiveras - error_issue_done_ratios_not_updated: "% klart inte uppdaterade." - error_workflow_copy_source: 'Vänligen välj källans ärendetyp eller roll' - error_workflow_copy_target: 'Vänligen välj ärendetyp(er) och roll(er) för mÃ¥l' - error_unable_delete_issue_status: 'Ärendestatus kunde inte tas bort' - error_unable_to_connect: "Kan inte ansluta (%{value})" - error_attachment_too_big: Denna fil kan inte laddas upp eftersom den överstiger maximalt tillÃ¥ten filstorlek (%{max_size}) - error_session_expired: "Din session har gÃ¥tt ut. Vänligen logga in pÃ¥ nytt." - warning_attachments_not_saved: "%{count} fil(er) kunde inte sparas." - - mail_subject_lost_password: "Ditt %{value} lösenord" - mail_body_lost_password: 'För att ändra ditt lösenord, klicka pÃ¥ följande länk:' - mail_subject_register: "Din %{value} kontoaktivering" - mail_body_register: 'För att aktivera ditt konto, klicka pÃ¥ följande länk:' - mail_body_account_information_external: "Du kan använda ditt %{value}-konto för att logga in." - mail_body_account_information: Din kontoinformation - mail_subject_account_activation_request: "%{value} begäran om kontoaktivering" - mail_body_account_activation_request: "En ny användare (%{value}) har registrerat sig och avvaktar ditt godkännande:" - mail_subject_reminder: "%{count} ärende(n) har deadline under de kommande %{days} dagarna" - mail_body_reminder: "%{count} ärende(n) som är tilldelat dig har deadline under de %{days} dagarna:" - mail_subject_wiki_content_added: "'%{id}' wikisida has lagts till" - mail_body_wiki_content_added: "The '%{id}' wikisida has lagts till av %{author}." - mail_subject_wiki_content_updated: "'%{id}' wikisida har uppdaterats" - mail_body_wiki_content_updated: "The '%{id}' wikisida har uppdaterats av %{author}." - - gui_validation_error: 1 fel - gui_validation_error_plural: "%{count} fel" - - field_name: Namn - field_description: Beskrivning - field_summary: Sammanfattning - field_is_required: Obligatorisk - field_firstname: Förnamn - field_lastname: Efternamn - field_mail: Mail - field_filename: Fil - field_filesize: Storlek - field_downloads: Nerladdningar - field_author: Författare - field_created_on: Skapad - field_updated_on: Uppdaterad - field_field_format: Format - field_is_for_all: För alla projekt - field_possible_values: Möjliga värden - field_regexp: Reguljärt uttryck - field_min_length: Minimilängd - field_max_length: Maxlängd - field_value: Värde - field_category: Kategori - field_title: Titel - field_project: Projekt - field_issue: Ärende - field_status: Status - field_notes: Anteckningar - field_is_closed: Ärendet är stängt - field_is_default: Standardvärde - field_tracker: Ärendetyp - field_subject: Ämne - field_due_date: Deadline - field_assigned_to: Tilldelad till - field_priority: Prioritet - field_fixed_version: VersionsmÃ¥l - field_user: Användare - field_principal: Principal - field_role: Roll - field_homepage: Hemsida - field_is_public: Publik - field_parent: Underprojekt till - field_is_in_roadmap: Visa ärenden i roadmap - field_login: Användarnamn - field_mail_notification: Mailnotifieringar - field_admin: Administratör - field_last_login_on: Senaste inloggning - field_language: SprÃ¥k - field_effective_date: Datum - field_password: Lösenord - field_new_password: Nytt lösenord - field_password_confirmation: Bekräfta lösenord - field_version: Version - field_type: Typ - field_host: Värddator - field_port: Port - field_account: Konto - field_base_dn: Bas-DN - field_attr_login: Inloggningsattribut - field_attr_firstname: Förnamnsattribut - field_attr_lastname: Efternamnsattribut - field_attr_mail: Mailattribut - field_onthefly: Skapa användare on-the-fly - field_start_date: Startdatum - field_done_ratio: "% Klart" - field_auth_source: Autentiseringsläge - field_hide_mail: Dölj min mailadress - field_comments: Kommentar - field_url: URL - field_start_page: Startsida - field_subproject: Underprojekt - field_hours: Timmar - field_activity: Aktivitet - field_spent_on: Datum - field_identifier: Identifierare - field_is_filter: Använd som filter - field_issue_to: Relaterade ärenden - field_delay: Fördröjning - field_assignable: Ärenden kan tilldelas denna roll - field_redirect_existing_links: Omdirigera existerande länkar - field_estimated_hours: Estimerad tid - field_column_names: Kolumner - field_time_entries: Spenderad tid - field_time_zone: Tidszon - field_searchable: Sökbar - field_default_value: Standardvärde - field_comments_sorting: Visa kommentarer - field_parent_title: Föräldersida - field_editable: Redigerbar - field_watcher: Bevakare - field_identity_url: OpenID URL - field_content: InnehÃ¥ll - field_group_by: Gruppera resultat efter - field_sharing: Delning - field_parent_issue: Förälderaktivitet - field_member_of_group: "Tilldelad användares grupp" - field_assigned_to_role: "Tilldelad användares roll" - field_text: Textfält - field_visible: Synlig - field_warn_on_leaving_unsaved: Varna om jag lämnar en sida med osparad text - field_issues_visibility: Ärendesynlighet - field_is_private: Privat - field_commit_logs_encoding: Teckenuppsättning för commit-meddelanden - field_scm_path_encoding: Sökvägskodning - field_path_to_repository: Sökväg till versionsarkiv - field_root_directory: Rotmapp - field_cvsroot: CVSROOT - field_cvs_module: Modul - field_repository_is_default: Huvudarkiv - field_multiple: Flera värden - field_auth_source_ldap_filter: LDAP-filter - field_core_fields: Standardfält - field_timeout: "Timeout (i sekunder)" - field_board_parent: Förälderforum - field_private_notes: Privata anteckningar - - setting_app_title: Applikationsrubrik - setting_app_subtitle: Applikationsunderrubrik - setting_welcome_text: Välkomsttext - setting_default_language: StandardsprÃ¥k - setting_login_required: Kräver inloggning - setting_self_registration: Självregistrering - setting_attachment_max_size: Maxstorlek pÃ¥ bilaga - setting_issues_export_limit: Exportgräns för ärenden - setting_mail_from: Avsändaradress - setting_bcc_recipients: Hemlig kopia (bcc) till mottagare - setting_plain_text_mail: Oformaterad text i mail (ingen HTML) - setting_host_name: Värddatornamn - setting_text_formatting: Textformatering - setting_wiki_compression: Komprimering av wikihistorik - setting_feeds_limit: InnehÃ¥llsgräns för Feed - setting_default_projects_public: Nya projekt är publika - setting_autofetch_changesets: Automatisk hämtning av commits - setting_sys_api_enabled: Aktivera WS för versionsarkivhantering - setting_commit_ref_keywords: Referens-nyckelord - setting_commit_fix_keywords: Fix-nyckelord - setting_autologin: Automatisk inloggning - setting_date_format: Datumformat - setting_time_format: Tidsformat - setting_cross_project_subtasks: TillÃ¥t underaktiviteter mellan projekt - setting_cross_project_issue_relations: TillÃ¥t ärenderelationer mellan projekt - setting_issue_list_default_columns: Standardkolumner i ärendelistan - setting_repositories_encodings: Encoding för bilagor och versionsarkiv - setting_emails_header: Mail-header - setting_emails_footer: Signatur - setting_protocol: Protokoll - setting_per_page_options: Alternativ, objekt per sida - setting_user_format: Visningsformat för användare - setting_activity_days_default: Dagar som visas pÃ¥ projektaktivitet - setting_display_subprojects_issues: Visa ärenden frÃ¥n underprojekt i huvudprojekt - setting_enabled_scm: Aktivera SCM - setting_mail_handler_body_delimiters: "Trunkera mail efter en av följande rader" - setting_mail_handler_api_enabled: Aktivera WS för inkommande mail - setting_mail_handler_api_key: API-nyckel - setting_sequential_project_identifiers: Generera projektidentifierare sekventiellt - setting_gravatar_enabled: Använd Gravatar-avatarer - setting_gravatar_default: Förvald Gravatar-bild - setting_diff_max_lines_displayed: Maximalt antal synliga rader i diff - setting_file_max_size_displayed: Maxstorlek pÃ¥ textfiler som visas inline - setting_repository_log_display_limit: Maximalt antal revisioner i filloggen - setting_openid: TillÃ¥t inloggning och registrering med OpenID - setting_password_min_length: Minsta tillÃ¥tna lösenordslängd - setting_new_project_user_role_id: Tilldelad roll för en icke-administratör som skapar ett projekt - setting_default_projects_modules: Aktiverade moduler för nya projekt - setting_issue_done_ratio: Beräkna % klart med - setting_issue_done_ratio_issue_field: Använd ärendefältet - setting_issue_done_ratio_issue_status: Använd ärendestatus - setting_start_of_week: Första dagen i veckan - setting_rest_api_enabled: Aktivera REST webbtjänst - setting_cache_formatted_text: Cacha formaterad text - setting_default_notification_option: Standard notifieringsalternativ - setting_commit_logtime_enabled: Aktivera tidloggning - setting_commit_logtime_activity_id: Aktivitet för loggad tid - setting_gantt_items_limit: Maximalt antal aktiviteter som visas i gantt-schemat - setting_issue_group_assignment: TillÃ¥t att ärenden tilldelas till grupper - setting_default_issue_start_date_to_creation_date: Använd dagens datum som startdatum för nya ärenden - setting_commit_cross_project_ref: TillÃ¥t ärende i alla de andra projekten att bli refererade och fixade - setting_unsubscribe: TillÃ¥t användare att avsluta prenumereration - setting_session_lifetime: Maximal sessionslivslängd - setting_session_timeout: Tidsgräns för sessionsinaktivitet - setting_thumbnails_enabled: Visa miniatyrbilder av bilagor - setting_thumbnails_size: Storlek pÃ¥ miniatyrbilder (i pixlar) - setting_non_working_week_days: Lediga dagar - - permission_add_project: Skapa projekt - permission_add_subprojects: Skapa underprojekt - permission_edit_project: Ändra projekt - permission_close_project: Stänga / Ã¥teröppna projektet - permission_select_project_modules: Välja projektmoduler - permission_manage_members: Hantera medlemmar - permission_manage_project_activities: Hantera projektaktiviteter - permission_manage_versions: Hantera versioner - permission_manage_categories: Hantera ärendekategorier - permission_add_issues: Lägga till ärenden - permission_edit_issues: Ändra ärenden - permission_view_issues: Visa ärenden - permission_manage_issue_relations: Hantera ärenderelationer - permission_set_issues_private: Sätta ärenden publika eller privata - permission_set_own_issues_private: Sätta egna ärenden publika eller privata - permission_add_issue_notes: Lägga till ärendenotering - permission_edit_issue_notes: Ändra ärendenoteringar - permission_edit_own_issue_notes: Ändra egna ärendenoteringar - permission_view_private_notes: Visa privata anteckningar - permission_set_notes_private: Ställa in anteckningar som privata - permission_move_issues: Flytta ärenden - permission_delete_issues: Ta bort ärenden - permission_manage_public_queries: Hantera publika frÃ¥gor - permission_save_queries: Spara frÃ¥gor - permission_view_gantt: Visa Gantt-schema - permission_view_calendar: Visa kalender - permission_view_issue_watchers: Visa bevakarlista - permission_add_issue_watchers: Lägga till bevakare - permission_delete_issue_watchers: Ta bort bevakare - permission_log_time: Logga spenderad tid - permission_view_time_entries: Visa spenderad tid - permission_edit_time_entries: Ändra tidloggningar - permission_edit_own_time_entries: Ändra egna tidloggningar - permission_manage_news: Hantera nyheter - permission_comment_news: Kommentera nyheter - permission_manage_documents: Hantera dokument - permission_view_documents: Visa dokument - permission_manage_files: Hantera filer - permission_view_files: Visa filer - permission_manage_wiki: Hantera wiki - permission_rename_wiki_pages: Byta namn pÃ¥ wikisidor - permission_delete_wiki_pages: Ta bort wikisidor - permission_view_wiki_pages: Visa wiki - permission_view_wiki_edits: Visa wikihistorik - permission_edit_wiki_pages: Ändra wikisidor - permission_delete_wiki_pages_attachments: Ta bort bilagor - permission_protect_wiki_pages: Skydda wikisidor - permission_manage_repository: Hantera versionsarkiv - permission_browse_repository: Bläddra i versionsarkiv - permission_view_changesets: Visa changesets - permission_commit_access: Commit-Ã¥tkomst - permission_manage_boards: Hantera forum - permission_view_messages: Visa meddelanden - permission_add_messages: Lägg till meddelanden - permission_edit_messages: Ändra meddelanden - permission_edit_own_messages: Ändra egna meddelanden - permission_delete_messages: Ta bort meddelanden - permission_delete_own_messages: Ta bort egna meddelanden - permission_export_wiki_pages: Exportera wikisidor - permission_manage_subtasks: Hantera underaktiviteter - permission_manage_related_issues: Hantera relaterade ärenden - - project_module_issue_tracking: Ärendeuppföljning - project_module_time_tracking: Tidsuppföljning - project_module_news: Nyheter - project_module_documents: Dokument - project_module_files: Filer - project_module_wiki: Wiki - project_module_repository: Versionsarkiv - project_module_boards: Forum - project_module_calendar: Kalender - project_module_gantt: Gantt - - label_user: Användare - label_user_plural: Användare - label_user_new: Ny användare - label_user_anonymous: Anonym - label_project: Projekt - label_project_new: Nytt projekt - label_project_plural: Projekt - label_x_projects: - zero: inga projekt - one: 1 projekt - other: "%{count} projekt" - label_project_all: Alla projekt - label_project_latest: Senaste projekt - label_issue: Ärende - label_issue_new: Nytt ärende - label_issue_plural: Ärenden - label_issue_view_all: Visa alla ärenden - label_issues_by: "Ärenden %{value}" - label_issue_added: Ärende tillagt - label_issue_updated: Ärende uppdaterat - label_issue_note_added: Anteckning tillagd - label_issue_status_updated: Status uppdaterad - label_issue_priority_updated: Prioritet uppdaterad - label_document: Dokument - label_document_new: Nytt dokument - label_document_plural: Dokument - label_document_added: Dokument tillagt - label_role: Roll - label_role_plural: Roller - label_role_new: Ny roll - label_role_and_permissions: Roller och behörigheter - label_role_anonymous: Anonym - label_role_non_member: Icke-medlem - label_member: Medlem - label_member_new: Ny medlem - label_member_plural: Medlemmar - label_tracker: Ärendetyp - label_tracker_plural: Ärendetyper - label_tracker_new: Ny ärendetyp - label_workflow: Arbetsflöde - label_issue_status: Ärendestatus - label_issue_status_plural: Ärendestatus - label_issue_status_new: Ny status - label_issue_category: Ärendekategori - label_issue_category_plural: Ärendekategorier - label_issue_category_new: Ny kategori - label_custom_field: Användardefinerat fält - label_custom_field_plural: Användardefinerade fält - label_custom_field_new: Nytt användardefinerat fält - label_enumerations: Uppräkningar - label_enumeration_new: Nytt värde - label_information: Information - label_information_plural: Information - label_please_login: Var god logga in - label_register: Registrera - label_login_with_open_id_option: eller logga in med OpenID - label_password_lost: Glömt lösenord - label_home: Hem - label_my_page: Min sida - label_my_account: Mitt konto - label_my_projects: Mina projekt - label_my_page_block: '"Min sida"-block' - label_administration: Administration - label_login: Logga in - label_logout: Logga ut - label_help: Hjälp - label_reported_issues: Rapporterade ärenden - label_assigned_to_me_issues: Ärenden tilldelade till mig - label_last_login: Senaste inloggning - label_registered_on: Registrerad - label_activity: Aktivitet - label_overall_activity: All aktivitet - label_user_activity: "Aktiviteter för %{value}" - label_new: Ny - label_logged_as: Inloggad som - label_environment: Miljö - label_authentication: Autentisering - label_auth_source: Autentiseringsläge - label_auth_source_new: Nytt autentiseringsläge - label_auth_source_plural: Autentiseringslägen - label_subproject_plural: Underprojekt - label_subproject_new: Nytt underprojekt - label_and_its_subprojects: "%{value} och dess underprojekt" - label_min_max_length: Min./Max.-längd - label_list: Lista - label_date: Datum - label_integer: Heltal - label_float: Flyttal - label_boolean: Boolean - label_string: Text - label_text: LÃ¥ng text - label_attribute: Attribut - label_attribute_plural: Attribut - label_download: "%{count} Nerladdning" - label_download_plural: "%{count} Nerladdningar" - label_no_data: Ingen data att visa - label_change_status: Ändra status - label_history: Historia - label_attachment: Fil - label_attachment_new: Ny fil - label_attachment_delete: Ta bort fil - label_attachment_plural: Filer - label_file_added: Fil tillagd - label_report: Rapport - label_report_plural: Rapporter - label_news: Nyhet - label_news_new: Lägg till nyhet - label_news_plural: Nyheter - label_news_latest: Senaste nyheterna - label_news_view_all: Visa alla nyheter - label_news_added: Nyhet tillagd - label_news_comment_added: Kommentar tillagd till en nyhet - label_settings: Inställningar - label_overview: Översikt - label_version: Version - label_version_new: Ny version - label_version_plural: Versioner - label_close_versions: Stäng klara versioner - label_confirmation: Bekräftelse - label_export_to: 'Finns även som:' - label_read: Läs... - label_public_projects: Publika projekt - label_open_issues: öppen - label_open_issues_plural: öppna - label_closed_issues: stängd - label_closed_issues_plural: stängda - label_x_open_issues_abbr_on_total: - zero: 0 öppna av %{total} - one: 1 öppen av %{total} - other: "%{count} öppna av %{total}" - label_x_open_issues_abbr: - zero: 0 öppna - one: 1 öppen - other: "%{count} öppna" - label_x_closed_issues_abbr: - zero: 0 stängda - one: 1 stängd - other: "%{count} stängda" - label_x_issues: - zero: 0 ärenden - one: 1 ärende - other: "%{count} ärenden" - label_total: Total - label_permissions: Behörigheter - label_current_status: Nuvarande status - label_new_statuses_allowed: Nya tillÃ¥tna statusvärden - label_all: alla - label_none: ingen - label_nobody: ingen - label_next: Nästa - label_previous: FöregÃ¥ende - label_used_by: Använd av - label_details: Detaljer - label_add_note: Lägg till anteckning - label_per_page: Per sida - label_calendar: Kalender - label_months_from: mÃ¥nader frÃ¥n - label_gantt: Gantt - label_internal: Intern - label_last_changes: "senaste %{count} ändringar" - label_change_view_all: Visa alla ändringar - label_personalize_page: Anpassa denna sida - label_comment: Kommentar - label_comment_plural: Kommentarer - label_x_comments: - zero: inga kommentarer - one: 1 kommentar - other: "%{count} kommentarer" - label_comment_add: Lägg till kommentar - label_comment_added: Kommentar tillagd - label_comment_delete: Ta bort kommentar - label_query: Användardefinerad frÃ¥ga - label_query_plural: Användardefinerade frÃ¥gor - label_query_new: Ny frÃ¥ga - label_my_queries: Mina egna frÃ¥gor - label_filter_add: Lägg till filter - label_filter_plural: Filter - label_equals: är - label_not_equals: är inte - label_in_less_than: om mindre än - label_in_more_than: om mer än - label_in_the_next_days: under kommande - label_in_the_past_days: under föregÃ¥ende - label_greater_or_equal: '>=' - label_less_or_equal: '<=' - label_between: mellan - label_in: om - label_today: idag - label_all_time: närsom - label_yesterday: igÃ¥r - label_this_week: denna vecka - label_last_week: senaste veckan - label_last_n_weeks: "senaste %{count} veckorna" - label_last_n_days: "senaste %{count} dagarna" - label_this_month: denna mÃ¥nad - label_last_month: senaste mÃ¥naden - label_this_year: detta Ã¥ret - label_date_range: Datumintervall - label_less_than_ago: mindre än dagar sedan - label_more_than_ago: mer än dagar sedan - label_ago: dagar sedan - label_contains: innehÃ¥ller - label_not_contains: innehÃ¥ller inte - label_any_issues_in_project: nÃ¥gra ärenden i projektet - label_any_issues_not_in_project: nÃ¥gra ärenden utanför projektet - label_no_issues_in_project: inga ärenden i projektet - label_day_plural: dagar - label_repository: Versionsarkiv - label_repository_new: Nytt versionsarkiv - label_repository_plural: Versionsarkiv - label_browse: Bläddra - label_modification: "%{count} ändring" - label_modification_plural: "%{count} ändringar" - label_branch: Branch - label_tag: Tag - label_revision: Revision - label_revision_plural: Revisioner - label_revision_id: "Revision %{value}" - label_associated_revisions: Associerade revisioner - label_added: tillagd - label_modified: modifierad - label_copied: kopierad - label_renamed: omdöpt - label_deleted: borttagen - label_latest_revision: Senaste revisionen - label_latest_revision_plural: Senaste revisionerna - label_view_revisions: Visa revisioner - label_view_all_revisions: Visa alla revisioner - label_max_size: Maxstorlek - label_sort_highest: Flytta till toppen - label_sort_higher: Flytta upp - label_sort_lower: Flytta ner - label_sort_lowest: Flytta till botten - label_roadmap: Roadmap - label_roadmap_due_in: "Färdig om %{value}" - label_roadmap_overdue: "%{value} sen" - label_roadmap_no_issues: Inga ärenden för denna version - label_search: Sök - label_result_plural: Resultat - label_all_words: Alla ord - label_wiki: Wiki - label_wiki_edit: Wikiändring - label_wiki_edit_plural: Wikiändringar - label_wiki_page: Wikisida - label_wiki_page_plural: Wikisidor - label_index_by_title: InnehÃ¥ll efter titel - label_index_by_date: InnehÃ¥ll efter datum - label_current_version: Nuvarande version - label_preview: Förhandsgranska - label_feed_plural: Feeds - label_changes_details: Detaljer om alla ändringar - label_issue_tracking: Ärendeuppföljning - label_spent_time: Spenderad tid - label_overall_spent_time: Total tid spenderad - label_f_hour: "%{value} timme" - label_f_hour_plural: "%{value} timmar" - label_time_tracking: Tidsuppföljning - label_change_plural: Ändringar - label_statistics: Statistik - label_commits_per_month: Commits per mÃ¥nad - label_commits_per_author: Commits per författare - label_diff: diff - label_view_diff: Visa skillnader - label_diff_inline: i texten - label_diff_side_by_side: sida vid sida - label_options: Inställningar - label_copy_workflow_from: Kopiera arbetsflöde frÃ¥n - label_permissions_report: Behörighetsrapport - label_watched_issues: Bevakade ärenden - label_related_issues: Relaterade ärenden - label_applied_status: Tilldelad status - label_loading: Laddar... - label_relation_new: Ny relation - label_relation_delete: Ta bort relation - label_relates_to: relaterar till - label_duplicates: kopierar - label_duplicated_by: kopierad av - label_blocks: blockerar - label_blocked_by: blockerad av - label_precedes: kommer före - label_follows: följer - label_copied_to: Kopierad till - label_copied_from: Kopierad frÃ¥n - label_end_to_start: slut till start - label_end_to_end: slut till slut - label_start_to_start: start till start - label_start_to_end: start till slut - label_stay_logged_in: Förbli inloggad - label_disabled: inaktiverad - label_show_completed_versions: Visa färdiga versioner - label_me: mig - label_board: Forum - label_board_new: Nytt forum - label_board_plural: Forum - label_board_locked: LÃ¥st - label_board_sticky: Sticky - label_topic_plural: Ämnen - label_message_plural: Meddelanden - label_message_last: Senaste meddelande - label_message_new: Nytt meddelande - label_message_posted: Meddelande tillagt - label_reply_plural: Svar - label_send_information: Skicka kontoinformation till användaren - label_year: Ã…r - label_month: MÃ¥nad - label_week: Vecka - label_date_from: FrÃ¥n - label_date_to: Till - label_language_based: SprÃ¥kbaserad - label_sort_by: "Sortera pÃ¥ %{value}" - label_send_test_email: Skicka testmail - label_feeds_access_key: RSS-nyckel - label_missing_feeds_access_key: Saknar en RSS-nyckel - label_feeds_access_key_created_on: "RSS-nyckel skapad för %{value} sedan" - label_module_plural: Moduler - label_added_time_by: "Tillagd av %{author} för %{age} sedan" - label_updated_time_by: "Uppdaterad av %{author} för %{age} sedan" - label_updated_time: "Uppdaterad för %{value} sedan" - label_jump_to_a_project: GÃ¥ till projekt... - label_file_plural: Filer - label_changeset_plural: Changesets - label_default_columns: Standardkolumner - label_no_change_option: (Ingen ändring) - label_bulk_edit_selected_issues: Gemensam ändring av markerade ärenden - label_bulk_edit_selected_time_entries: Gruppredigera valda tidloggningar - label_theme: Tema - label_default: Standard - label_search_titles_only: Sök endast i titlar - label_user_mail_option_all: "För alla händelser i mina projekt" - label_user_mail_option_selected: "För alla händelser i markerade projekt..." - label_user_mail_option_none: "Inga händelser" - label_user_mail_option_only_my_events: "Endast för saker jag bevakar eller är inblandad i" - label_user_mail_option_only_assigned: "Endast för saker jag är tilldelad" - label_user_mail_option_only_owner: "Endast för saker jag äger" - label_user_mail_no_self_notified: "Jag vill inte bli underrättad om ändringar som jag har gjort" - label_registration_activation_by_email: kontoaktivering med mail - label_registration_manual_activation: manuell kontoaktivering - label_registration_automatic_activation: automatisk kontoaktivering - label_display_per_page: "Per sida: %{value}" - label_age: Ã…lder - label_change_properties: Ändra inställningar - label_general: Allmänt - label_more: Mer - label_scm: SCM - label_plugins: Tillägg - label_ldap_authentication: LDAP-autentisering - label_downloads_abbr: Nerl. - label_optional_description: Valfri beskrivning - label_add_another_file: Lägg till ytterligare en fil - label_preferences: Användarinställningar - label_chronological_order: I kronologisk ordning - label_reverse_chronological_order: I omvänd kronologisk ordning - label_planning: Planering - label_incoming_emails: Inkommande mail - label_generate_key: Generera en nyckel - label_issue_watchers: Bevakare - label_example: Exempel - label_display: Visa - label_sort: Sortera - label_descending: Fallande - label_ascending: Stigande - label_date_from_to: FrÃ¥n %{start} till %{end} - label_wiki_content_added: Wikisida tillagd - label_wiki_content_updated: Wikisida uppdaterad - label_group: Grupp - label_group_plural: Grupper - label_group_new: Ny grupp - label_time_entry_plural: Spenderad tid - label_version_sharing_none: Inte delad - label_version_sharing_descendants: Med underprojekt - label_version_sharing_hierarchy: Med projekthierarki - label_version_sharing_tree: Med projektträd - label_version_sharing_system: Med alla projekt - label_update_issue_done_ratios: Uppdatera % klart - label_copy_source: Källa - label_copy_target: MÃ¥l - label_copy_same_as_target: Samma som mÃ¥l - label_display_used_statuses_only: Visa endast status som används av denna ärendetyp - label_api_access_key: API-nyckel - label_missing_api_access_key: Saknar en API-nyckel - label_api_access_key_created_on: "API-nyckel skapad för %{value} sedan" - label_profile: Profil - label_subtask_plural: Underaktiviteter - label_project_copy_notifications: Skicka mailnotifieringar när projektet kopieras - label_principal_search: "Sök efter användare eller grupp:" - label_user_search: "Sök efter användare:" - label_additional_workflow_transitions_for_author: Ytterligare övergÃ¥ngar tillÃ¥tna när användaren är den som skapat ärendet - label_additional_workflow_transitions_for_assignee: Ytterligare övergÃ¥ngar tillÃ¥tna när användaren är den som tilldelats ärendet - label_issues_visibility_all: Alla ärenden - label_issues_visibility_public: Alla icke-privata ärenden - label_issues_visibility_own: Ärenden skapade av eller tilldelade till användaren - label_git_report_last_commit: Rapportera senaste commit av filer och mappar - label_parent_revision: Förälder - label_child_revision: Barn - label_export_options: "%{export_format} exportalternativ" - label_copy_attachments: Kopiera bilagor - label_copy_subtasks: Kopiera underaktiviteter - label_item_position: "%{position}/%{count}" - label_completed_versions: Klara versioner - label_search_for_watchers: Sök efter bevakare att lägga till - label_session_expiration: SessionsutgÃ¥ng - label_show_closed_projects: Visa stängda projekt - label_status_transitions: StatusövergÃ¥ngar - label_fields_permissions: Fältbehörigheter - label_readonly: Skrivskyddad - label_required: Nödvändig - label_attribute_of_project: Projektets %{name} - label_attribute_of_author: Författarens %{name} - label_attribute_of_assigned_to: Tilldelads %{name} - label_attribute_of_fixed_version: MÃ¥lversionens %{name} - - button_login: Logga in - button_submit: Skicka - button_save: Spara - button_check_all: Markera alla - button_uncheck_all: Avmarkera alla - button_collapse_all: Kollapsa alla - button_expand_all: Expandera alla - button_delete: Ta bort - button_create: Skapa - button_create_and_continue: Skapa och fortsätt - button_test: Testa - button_edit: Ändra - button_edit_associated_wikipage: "Ändra associerad Wikisida: %{page_title}" - button_add: Lägg till - button_change: Ändra - button_apply: Verkställ - button_clear: Ã…terställ - button_lock: LÃ¥s - button_unlock: LÃ¥s upp - button_download: Ladda ner - button_list: Lista - button_view: Visa - button_move: Flytta - button_move_and_follow: Flytta och följ efter - button_back: Tillbaka - button_cancel: Avbryt - button_activate: Aktivera - button_sort: Sortera - button_log_time: Logga tid - button_rollback: Ã…terställ till denna version - button_watch: Bevaka - button_unwatch: Stoppa bevakning - button_reply: Svara - button_archive: Arkivera - button_unarchive: Ta bort frÃ¥n arkiv - button_reset: Ã…terställ - button_rename: Byt namn - button_change_password: Ändra lösenord - button_copy: Kopiera - button_copy_and_follow: Kopiera och följ efter - button_annotate: Kommentera - button_update: Uppdatera - button_configure: Konfigurera - button_quote: Citera - button_duplicate: Duplicera - button_show: Visa - button_hide: Göm - button_edit_section: Redigera denna sektion - button_export: Exportera - button_delete_my_account: Ta bort mitt konto - button_close: Stäng - button_reopen: Ã…teröppna - - status_active: aktiv - status_registered: registrerad - status_locked: lÃ¥st - - project_status_active: aktiv - project_status_closed: stängd - project_status_archived: arkiverad - - version_status_open: öppen - version_status_locked: lÃ¥st - version_status_closed: stängd - - field_active: Aktiv - - text_select_mail_notifications: Välj för vilka händelser mail ska skickas. - text_regexp_info: eg. ^[A-Z0-9]+$ - text_min_max_length_info: 0 betyder ingen gräns - text_project_destroy_confirmation: Är du säker pÃ¥ att du vill ta bort detta projekt och all relaterad data? - text_subprojects_destroy_warning: "Alla underprojekt: %{value} kommer ocksÃ¥ tas bort." - text_workflow_edit: Välj en roll och en ärendetyp för att ändra arbetsflöde - text_are_you_sure: Är du säker ? - text_journal_changed: "%{label} ändrad frÃ¥n %{old} till %{new}" - text_journal_changed_no_detail: "%{label} uppdaterad" - text_journal_set_to: "%{label} satt till %{value}" - text_journal_deleted: "%{label} borttagen (%{old})" - text_journal_added: "%{label} %{value} tillagd" - text_tip_issue_begin_day: ärende som börjar denna dag - text_tip_issue_end_day: ärende som slutar denna dag - text_tip_issue_begin_end_day: ärende som börjar och slutar denna dag - text_project_identifier_info: Ändast gemener (a-z), siffror, streck och understreck är tillÃ¥tna.
    När identifieraren sparats kan den inte ändras. - text_caracters_maximum: "max %{count} tecken." - text_caracters_minimum: "Måste vara minst %{count} tecken lång." - text_length_between: "Längd mellan %{min} och %{max} tecken." - text_tracker_no_workflow: Inget arbetsflöde definerat för denna ärendetyp - text_unallowed_characters: Otillåtna tecken - text_comma_separated: Flera värden tillåtna (kommaseparerade). - text_line_separated: Flera värden tillåtna (ett värde per rad). - text_issues_ref_in_commit_messages: Referera och fixa ärenden i commit-meddelanden - text_issue_added: "Ärende %{id} har rapporterats (av %{author})." - text_issue_updated: "Ärende %{id} har uppdaterats (av %{author})." - text_wiki_destroy_confirmation: Är du säker på att du vill ta bort denna wiki och allt dess innehåll ? - text_issue_category_destroy_question: "Några ärenden (%{count}) är tilldelade till denna kategori. Vad vill du göra ?" - text_issue_category_destroy_assignments: Ta bort kategoritilldelningar - text_issue_category_reassign_to: Återtilldela ärenden till denna kategori - text_user_mail_option: "För omarkerade projekt kommer du bara bli underrättad om saker du bevakar eller är inblandad i (T.ex. ärenden du skapat eller tilldelats)." - text_no_configuration_data: "Roller, ärendetyper, ärendestatus och arbetsflöden har inte konfigurerats ännu.\nDet rekommenderas att läsa in standardkonfigurationen. Du kommer att kunna göra ändringar efter att den blivit inläst." - text_load_default_configuration: Läs in standardkonfiguration - text_status_changed_by_changeset: "Tilldelad i changeset %{value}." - text_time_logged_by_changeset: "Tilldelad i changeset %{value}." - text_issues_destroy_confirmation: 'Är du säker på att du vill radera markerade ärende(n) ?' - text_issues_destroy_descendants_confirmation: Detta kommer även ta bort %{count} underaktivitet(er). - text_time_entries_destroy_confirmation: Är du säker på att du vill ta bort valda tidloggningar? - text_select_project_modules: 'Välj vilka moduler som ska vara aktiva för projektet:' - text_default_administrator_account_changed: Standardadministratörens konto ändrat - text_file_repository_writable: Arkivet för bifogade filer är skrivbart - text_plugin_assets_writable: Arkivet för plug-ins är skrivbart - text_rmagick_available: RMagick tillgängligt (ej obligatoriskt) - text_destroy_time_entries_question: "%{hours} timmar har rapporterats på ärendena du är på väg att ta bort. Vad vill du göra ?" - text_destroy_time_entries: Ta bort rapporterade timmar - text_assign_time_entries_to_project: Tilldela rapporterade timmar till projektet - text_reassign_time_entries: 'Återtilldela rapporterade timmar till detta ärende:' - text_user_wrote: "%{value} skrev:" - text_enumeration_destroy_question: "%{count} objekt är tilldelade till detta värde." - text_enumeration_category_reassign_to: 'Återtilldela till detta värde:' - text_email_delivery_not_configured: "Mailfunktionen har inte konfigurerats, och notifieringar via mail kan därför inte skickas.\nKonfigurera din SMTP-server i config/configuration.yml och starta om applikationen för att aktivera dem." - text_repository_usernames_mapping: "Välj eller uppdatera den Redmine-användare som är mappad till varje användarnamn i versionarkivloggen.\nAnvändare med samma användarnamn eller mailadress i både Redmine och versionsarkivet mappas automatiskt." - text_diff_truncated: '... Denna diff har förminskats eftersom den överskrider den maximala storlek som kan visas.' - text_custom_field_possible_values_info: 'Ett värde per rad' - text_wiki_page_destroy_question: "Denna sida har %{descendants} underliggande sidor. Vad vill du göra?" - text_wiki_page_nullify_children: "Behåll undersidor som rotsidor" - text_wiki_page_destroy_children: "Ta bort alla underliggande sidor" - text_wiki_page_reassign_children: "Flytta undersidor till denna föräldersida" - text_own_membership_delete_confirmation: "Några av, eller alla, dina behörigheter kommer att tas bort och du kanske inte längre kommer kunna göra ändringar i det här projektet.\nVill du verkligen fortsätta?" - text_zoom_out: Zooma ut - text_zoom_in: Zooma in - text_warn_on_leaving_unsaved: Nuvarande sida innehåller osparad text som kommer försvinna om du lämnar sidan. - text_scm_path_encoding_note: "Standard: UTF-8" - text_git_repository_note: Versionsarkiv är tomt och lokalt (t.ex. /gitrepo, c:\gitrepo) - text_mercurial_repository_note: Lokalt versionsarkiv (t.ex. /hgrepo, c:\hgrepo) - text_scm_command: Kommando - text_scm_command_version: Version - text_scm_config: Du kan konfigurera dina scm-kommando i config/configuration.yml. Vänligen starta om applikationen när ändringar gjorts. - text_scm_command_not_available: Scm-kommando är inte tillgängligt. Vänligen kontrollera inställningarna i administratörspanelen. - text_issue_conflict_resolution_overwrite: Använd mina ändringar i alla fall (tidigare anteckningar kommer behållas men några ändringar kan bli överskrivna) - text_issue_conflict_resolution_add_notes: Lägg till mina anteckningar och kasta mina andra ändringar - text_issue_conflict_resolution_cancel: Kasta alla mina ändringar och visa igen %{link} - text_account_destroy_confirmation: "Är du säker på att du vill fortsätta?\nDitt konto kommer tas bort permanent, utan möjlighet att återaktivera det." - text_session_expiration_settings: "Varning: ändring av dessa inställningar kan få alla nuvarande sessioner, inklusive din egen, att gå ut." - text_project_closed: Detta projekt är stängt och skrivskyddat. - - default_role_manager: Projektledare - default_role_developer: Utvecklare - default_role_reporter: Rapportör - default_tracker_bug: Bugg - default_tracker_feature: Funktionalitet - default_tracker_support: Support - default_issue_status_new: Ny - default_issue_status_in_progress: Pågår - default_issue_status_resolved: Löst - default_issue_status_feedback: Återkoppling - default_issue_status_closed: Stängd - default_issue_status_rejected: Avslagen - default_doc_category_user: Användardokumentation - default_doc_category_tech: Teknisk dokumentation - default_priority_low: Låg - default_priority_normal: Normal - default_priority_high: Hög - default_priority_urgent: Brådskande - default_priority_immediate: Omedelbar - default_activity_design: Design - default_activity_development: Utveckling - - enumeration_issue_priorities: Ärendeprioriteter - enumeration_doc_categories: Dokumentkategorier - enumeration_activities: Aktiviteter (tidsuppföljning) - enumeration_system_activity: Systemaktivitet - description_filter: Filter - description_search: Sökfält - description_choose_project: Projekt - description_project_scope: Sökomfång - description_notes: Anteckningar - description_message_content: Meddelandeinnehåll - description_query_sort_criteria_attribute: Sorteringsattribut - description_query_sort_criteria_direction: Sorteringsriktning - description_user_mail_notification: Mailnotifieringsinställningar - description_available_columns: Tillgängliga Kolumner - description_selected_columns: Valda Kolumner - description_all_columns: Alla kolumner - description_issue_category_reassign: Välj ärendekategori - description_wiki_subpages_reassign: Välj ny föräldersida - description_date_range_list: Välj intervall från listan - description_date_range_interval: Ange intervall genom att välja start- och slutdatum - description_date_from: Ange startdatum - description_date_to: Ange slutdatum - text_repository_identifier_info: Ändast gemener (a-z), siffror, streck och understreck är tillåtna.
    När identifieraren sparats kan den inte ändras. - label_any: alla - label_cross_project_descendants: Med underprojekt - label_cross_project_tree: Med projektträd - label_cross_project_hierarchy: Med projekthierarki - label_cross_project_system: Med alla projekt diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15d709b3f6a05dc0aade44cd45030b0d995c1fdc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/15d709b3f6a05dc0aade44cd45030b0d995c1fdc.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,23 @@ + + +Redmine 404 error + + +

    Page not found

    +

    The page you were trying to access doesn't exist or has been removed.

    +

    Back

    + + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15d89251506baa28cfc583d29fd309d36051242b.svn-base --- a/.svn/pristine/15/15d89251506baa28cfc583d29fd309d36051242b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2012 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 CalendarsHelper - def link_to_previous_month(year, month, options={}) - target_year, target_month = if month == 1 - [year - 1, 12] - else - [year, month - 1] - end - - name = if target_month == 12 - "#{month_name(target_month)} #{target_year}" - else - "#{month_name(target_month)}" - end - - # \xc2\xab(utf-8) = « - link_to_month(("\xc2\xab " + name), target_year, target_month, options) - end - - def link_to_next_month(year, month, options={}) - target_year, target_month = if month == 12 - [year + 1, 1] - else - [year, month + 1] - end - - name = if target_month == 1 - "#{month_name(target_month)} #{target_year}" - else - "#{month_name(target_month)}" - end - - # \xc2\xbb(utf-8) = » - link_to_month((name + " \xc2\xbb"), target_year, target_month, options) - end - - def link_to_month(link_name, year, month, options={}) - link_to_content_update(h(link_name), params.merge(:year => year, :month => month)) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15eea4ed19b710f600770704c4c93d19262f8634.svn-base --- a/.svn/pristine/15/15eea4ed19b710f600770704c4c93d19262f8634.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 TimeEntryCustomField < CustomField - def type_name - :label_spent_time - end -end - diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15f48af3113e93bba083d75f20a6f8b663df8337.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/15f48af3113e93bba083d75f20a6f8b663df8337.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 WorkflowRule < ActiveRecord::Base + self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}" + + belongs_to :role + belongs_to :tracker + belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id' + belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' + + validates_presence_of :role, :tracker, :old_status + + # Copies workflows from source to targets + def self.copy(source_tracker, source_role, target_trackers, target_roles) + unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) + raise ArgumentError.new("source_tracker or source_role must be specified") + end + + target_trackers = [target_trackers].flatten.compact + target_roles = [target_roles].flatten.compact + + target_trackers = Tracker.sorted.all if target_trackers.empty? + target_roles = Role.all if target_roles.empty? + + target_trackers.each do |target_tracker| + target_roles.each do |target_role| + copy_one(source_tracker || target_tracker, + source_role || target_role, + target_tracker, + target_role) + end + end + end + + # Copies a single set of workflows from source to target + def self.copy_one(source_tracker, source_role, target_tracker, target_role) + unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? && + source_role.is_a?(Role) && !source_role.new_record? && + target_tracker.is_a?(Tracker) && !target_tracker.new_record? && + target_role.is_a?(Role) && !target_role.new_record? + + raise ArgumentError.new("arguments can not be nil or unsaved objects") + end + + if source_tracker == target_tracker && source_role == target_role + false + else + transaction do + delete_all :tracker_id => target_tracker.id, :role_id => target_role.id + connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type)" + + " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type" + + " FROM #{WorkflowRule.table_name}" + + " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}" + end + true + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/15/15fb06eaccea2e7a2a26f4f458f36f318208cf74.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/15/15fb06eaccea2e7a2a26f4f458f36f318208cf74.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,18 @@ +<%= form_for @project, + :url => { :action => 'modules', :id => @project }, + :html => {:id => 'modules-form', + :method => :post} do |f| %> + +
    +<%= l(:text_select_project_modules) %> + +<% Redmine::AccessControl.available_project_modules.each do |m| %> +

    +<% end %> +
    +

    <%= check_all_links 'modules-form' %>

    + +

    <%= submit_tag l(:button_save) %>

    + +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/1600d50ad943bf7aaab25e360ad342f24d555f94.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/16/1600d50ad943bf7aaab25e360ad342f24d555f94.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1118 @@ +sl: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + 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: "%d.%m.%Y" + short: "%d. %b" + long: "%d. %B, %Y" + + day_names: [Nedelja, Ponedeljek, Torek, Sreda, ÄŒetrtek, Petek, Sobota] + abbr_day_names: [Ned, Pon, To, Sr, ÄŒet, Pet, Sob] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Januar, Februar, Marec, April, Maj, Junij, Julij, Avgust, September, Oktober, November, December] + abbr_month_names: [~, Jan, Feb, Mar, Apr, Maj, Jun, Jul, Aug, Sep, Okt, Nov, Dec] + # Used in date_select and datime_select. + order: + - :day + - :month + - :year + + time: + formats: + default: "%a, %d. %b, %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d. %b %H:%M" + long: "%d. %B, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "pol minute" + less_than_x_seconds: + one: "manj kot 1. sekundo" + other: "manj kot %{count} sekund" + x_seconds: + one: "1. sekunda" + other: "%{count} sekund" + less_than_x_minutes: + one: "manj kot minuto" + other: "manj kot %{count} minut" + x_minutes: + one: "1 minuta" + other: "%{count} minut" + about_x_hours: + one: "okrog 1. ure" + other: "okrog %{count} ur" + x_hours: + one: "1 ura" + other: "%{count} ur" + x_days: + one: "1 dan" + other: "%{count} dni" + about_x_months: + one: "okrog 1. mesec" + other: "okrog %{count} mesecev" + x_months: + one: "1 mesec" + other: "%{count} mesecev" + about_x_years: + one: "okrog 1. leto" + other: "okrog %{count} let" + over_x_years: + one: "veÄ kot 1. leto" + other: "veÄ kot %{count} let" + almost_x_years: + one: "skoraj 1. leto" + other: "skoraj %{count} let" + + number: + format: + separator: "," + delimiter: "." + precision: 3 + human: + format: + precision: 3 + delimiter: "" + storage_units: + format: "%n %u" + units: + kb: KB + tb: TB + gb: GB + byte: + one: Byte + other: Bytes + mb: MB + +# Used in array.to_sentence. + support: + array: + sentence_connector: "in" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1. napaka je prepreÄila temu %{model} da bi se shranil" + other: "%{count} napak je prepreÄilo temu %{model} da bi se shranil" + messages: + inclusion: "ni vkljuÄen na seznamu" + exclusion: "je rezerviran" + invalid: "je napaÄen" + confirmation: "ne ustreza potrdilu" + accepted: "mora biti sprejet" + empty: "ne sme biti prazen" + blank: "ne sme biti neizpolnjen" + too_long: "je predolg" + too_short: "je prekratek" + wrong_length: "je napaÄne dolžine" + taken: "je že zaseden" + not_a_number: "ni Å¡tevilo" + not_a_date: "ni veljaven datum" + greater_than: "mora biti veÄji kot %{count}" + greater_than_or_equal_to: "mora biti veÄji ali enak kot %{count}" + equal_to: "mora biti enak kot %{count}" + less_than: "mora biti manjÅ¡i kot %{count}" + less_than_or_equal_to: "mora biti manjÅ¡i ali enak kot %{count}" + odd: "mora biti sodo" + even: "mora biti liho" + greater_than_start_date: "mora biti kasnejÅ¡i kot zaÄetni datum" + not_same_project: "ne pripada istemu projektu" + circular_dependency: "Ta odnos bi povzroÄil krožno odvisnost" + cant_link_an_issue_with_a_descendant: "Zahtevek ne more biti povezan s svojo podnalogo" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Prosimo izberite + + general_text_No: 'Ne' + general_text_Yes: 'Da' + general_text_no: 'ne' + general_text_yes: 'da' + general_lang_name: 'SlovenÅ¡Äina' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: RaÄun je bil uspeÅ¡no posodobljen. + notice_account_invalid_creditentials: NapaÄno uporabniÅ¡ko ime ali geslo + notice_account_password_updated: Geslo je bilo uspeÅ¡no posodobljeno. + notice_account_wrong_password: NapaÄno geslo + notice_account_register_done: RaÄun je bil uspeÅ¡no ustvarjen. Za aktivacijo potrdite povezavo, ki vam je bila poslana v e-nabiralnik. + notice_account_unknown_email: Neznan uporabnik. + notice_can_t_change_password: Ta raÄun za overovljanje uporablja zunanji. Gesla ni mogoÄe spremeniti. + notice_account_lost_email_sent: Poslano vam je bilo e-pismo z navodili za izbiro novega gesla. + notice_account_activated: VaÅ¡ raÄun je bil aktiviran. Sedaj se lahko prijavite. + notice_successful_create: Ustvarjanje uspelo. + notice_successful_update: Posodobitev uspela. + notice_successful_delete: Izbris uspel. + notice_successful_connection: Povezava uspela. + notice_file_not_found: Stran na katero se želite povezati ne obstaja ali pa je bila umaknjena. + notice_locking_conflict: Drug uporabnik je posodobil podatke. + notice_not_authorized: Nimate privilegijev za dostop do te strani. + notice_email_sent: "E-poÅ¡tno sporoÄilo je bilo poslano %{value}" + notice_email_error: "Ob poÅ¡iljanju e-sporoÄila je priÅ¡lo do napake (%{value})" + notice_feeds_access_key_reseted: VaÅ¡ Atom dostopni kljuÄ je bil ponastavljen. + notice_failed_to_save_issues: "Neuspelo shranjevanje %{count} zahtevka na %{total} izbranem: %{ids}." + notice_no_issue_selected: "Izbran ni noben zahtevek! Prosimo preverite zahtevke, ki jih želite urediti." + notice_account_pending: "VaÅ¡ raÄun je bil ustvarjen in Äaka na potrditev s strani administratorja." + notice_default_data_loaded: Privzete nastavitve so bile uspeÅ¡no naložene. + notice_unable_delete_version: Verzije ni bilo mogoÄe izbrisati. + + error_can_t_load_default_data: "Privzetih nastavitev ni bilo mogoÄe naložiti: %{value}" + error_scm_not_found: "Vnos ali revizija v shrambi ni bila najdena ." + error_scm_command_failed: "Med vzpostavljem povezave s shrambo je priÅ¡lo do napake: %{value}" + error_scm_annotate: "Vnos ne obstaja ali pa ga ni mogoÄe komentirati." + error_issue_not_found_in_project: 'Zahtevek ni bil najden ali pa ne pripada temu projektu' + + mail_subject_lost_password: "VaÅ¡e %{value} geslo" + mail_body_lost_password: 'Za spremembo glesla kliknite na naslednjo povezavo:' + mail_subject_register: "Aktivacija %{value} vaÅ¡ega raÄuna" + mail_body_register: 'Za aktivacijo vaÅ¡ega raÄuna kliknite na naslednjo povezavo:' + mail_body_account_information_external: "Za prijavo lahko uporabite vaÅ¡ %{value} raÄun." + mail_body_account_information: "Informacije o vaÅ¡em raÄunu. Za spremembo gesla sledite linku 'Spremeni geslo' na naslovu za prijavo, spodaj." + mail_subject_account_activation_request: "%{value} zahtevek za aktivacijo raÄuna" + mail_body_account_activation_request: "Registriral se je nov uporabnik (%{value}). RaÄun Äaka na vaÅ¡o odobritev:" + mail_subject_reminder: "%{count} zahtevek(zahtevki) zapadejo v naslednjih %{days} dneh" + mail_body_reminder: "%{count} zahtevek(zahtevki), ki so vam dodeljeni bodo zapadli v naslednjih %{days} dneh:" + + + field_name: Ime + field_description: Opis + field_summary: Povzetek + field_is_required: Zahtevano + field_firstname: Ime + field_lastname: Priimek + field_mail: E-naslov + field_filename: Datoteka + field_filesize: Velikost + field_downloads: Prenosi + field_author: Avtor + field_created_on: Ustvarjen + field_updated_on: Posodobljeno + field_field_format: Format + field_is_for_all: Za vse projekte + field_possible_values: Možne vrednosti + field_regexp: Regularni izraz + field_min_length: Minimalna dolžina + field_max_length: Maksimalna dolžina + field_value: Vrednost + field_category: Kategorija + field_title: Naslov + field_project: Projekt + field_issue: Zahtevek + field_status: Status + field_notes: Zabeležka + field_is_closed: Zahtevek zaprt + field_is_default: Privzeta vrednost + field_tracker: Vrsta zahtevka + field_subject: Tema + field_due_date: Do datuma + field_assigned_to: Dodeljen + field_priority: Prioriteta + field_fixed_version: Ciljna verzija + field_user: Uporabnik + field_role: Vloga + field_homepage: DomaÄa stran + field_is_public: Javno + field_parent: Podprojekt projekta + field_is_in_roadmap: Zahtevki prikazani na zemljevidu + field_login: Prijava + field_mail_notification: E-poÅ¡tna oznanila + field_admin: Administrator + field_last_login_on: ZadnjiÄ povezan(a) + field_language: Jezik + field_effective_date: Datum + field_password: Geslo + field_new_password: Novo geslo + field_password_confirmation: Potrditev + field_version: Verzija + field_type: Tip + field_host: Gostitelj + field_port: Vrata + field_account: RaÄun + field_base_dn: Bazni DN + field_attr_login: Oznaka za prijavo + field_attr_firstname: Oznaka za ime + field_attr_lastname: Oznaka za priimek + field_attr_mail: Oznaka za e-naslov + field_onthefly: Sprotna izdelava uporabnikov + field_start_date: ZaÄetek + field_done_ratio: "% Narejeno" + field_auth_source: NaÄin overovljanja + field_hide_mail: Skrij moj e-naslov + field_comments: Komentar + field_url: URL + field_start_page: ZaÄetna stran + field_subproject: Podprojekt + field_hours: Ur + field_activity: Aktivnost + field_spent_on: Datum + field_identifier: Identifikator + field_is_filter: Uporabljen kot filter + field_issue_to: Povezan zahtevek + field_delay: Zamik + field_assignable: Zahtevki so lahko dodeljeni tej vlogi + field_redirect_existing_links: Preusmeri obstojeÄe povezave + field_estimated_hours: Ocenjen Äas + field_column_names: Stolpci + field_time_zone: ÄŒasovni pas + field_searchable: Zmožen iskanja + field_default_value: Privzeta vrednost + field_comments_sorting: Prikaži komentarje + field_parent_title: MatiÄna stran + + setting_app_title: Naslov aplikacije + setting_app_subtitle: Podnaslov aplikacije + setting_welcome_text: Pozdravno besedilo + setting_default_language: Privzeti jezik + setting_login_required: Zahtevano overovljanje + setting_self_registration: Samostojna registracija + setting_attachment_max_size: Maksimalna velikost priponk + setting_issues_export_limit: Skrajna meja za izvoz zahtevkov + setting_mail_from: E-naslov za emisijo + setting_bcc_recipients: Prejemniki slepih kopij (bcc) + setting_plain_text_mail: navadno e-sporoÄilo (ne HTML) + setting_host_name: Ime gostitelja in pot + setting_text_formatting: Oblikovanje besedila + setting_wiki_compression: Stiskanje Wiki zgodovine + setting_feeds_limit: Meja obsega Atom virov + setting_default_projects_public: Novi projekti so privzeto javni + setting_autofetch_changesets: Samodejni izvleÄek zapisa sprememb + setting_sys_api_enabled: OmogoÄi WS za upravljanje shrambe + setting_commit_ref_keywords: Sklicne kljuÄne besede + setting_commit_fix_keywords: Urejanje kljuÄne besede + setting_autologin: Avtomatska prijava + setting_date_format: Oblika datuma + setting_time_format: Oblika Äasa + setting_cross_project_issue_relations: Dovoli povezave zahtevkov med razliÄnimi projekti + setting_issue_list_default_columns: Privzeti stolpci prikazani na seznamu zahtevkov + setting_emails_footer: Noga e-sporoÄil + setting_protocol: Protokol + setting_per_page_options: Å tevilo elementov na stran + setting_user_format: Oblika prikaza uporabnikov + setting_activity_days_default: Prikaz dni na aktivnost projekta + setting_display_subprojects_issues: Privzeti prikaz zahtevkov podprojektov v glavnem projektu + setting_enabled_scm: OmogoÄen SCM + setting_mail_handler_api_enabled: OmogoÄi WS za prihajajoÄo e-poÅ¡to + setting_mail_handler_api_key: API kljuÄ + setting_sequential_project_identifiers: Generiraj projektne identifikatorje sekvenÄno + setting_gravatar_enabled: Uporabljaj Gravatar ikone + setting_diff_max_lines_displayed: Maksimalno Å¡tevilo prikazanih vrstic razliÄnosti + + permission_edit_project: Uredi projekt + permission_select_project_modules: Izberi module projekta + permission_manage_members: Uredi Älane + permission_manage_versions: Uredi verzije + permission_manage_categories: Urejanje kategorij zahtevkov + permission_add_issues: Dodaj zahtevke + permission_edit_issues: Uredi zahtevke + permission_manage_issue_relations: Uredi odnose med zahtevki + permission_add_issue_notes: Dodaj zabeležke + permission_edit_issue_notes: Uredi zabeležke + permission_edit_own_issue_notes: Uredi lastne zabeležke + permission_move_issues: Premakni zahtevke + permission_delete_issues: IzbriÅ¡i zahtevke + permission_manage_public_queries: Uredi javna povpraÅ¡evanja + permission_save_queries: Shrani povpraÅ¡evanje + permission_view_gantt: Poglej gantogram + permission_view_calendar: Poglej koledar + permission_view_issue_watchers: Oglej si listo spremeljevalcev + permission_add_issue_watchers: Dodaj spremljevalce + permission_log_time: Beleži porabljen Äas + permission_view_time_entries: Poglej porabljen Äas + permission_edit_time_entries: Uredi beležko Äasa + permission_edit_own_time_entries: Uredi beležko lastnega Äasa + permission_manage_news: Uredi novice + permission_comment_news: Komentiraj novice + permission_view_documents: Poglej dokumente + permission_manage_files: Uredi datoteke + permission_view_files: Poglej datoteke + permission_manage_wiki: Uredi wiki + permission_rename_wiki_pages: Preimenuj wiki strani + permission_delete_wiki_pages: IzbriÅ¡i wiki strani + permission_view_wiki_pages: Poglej wiki + permission_view_wiki_edits: Poglej wiki zgodovino + permission_edit_wiki_pages: Uredi wiki strani + permission_delete_wiki_pages_attachments: IzbriÅ¡i priponke + permission_protect_wiki_pages: ZaÅ¡Äiti wiki strani + permission_manage_repository: Uredi shrambo + permission_browse_repository: Prebrskaj shrambo + permission_view_changesets: Poglej zapis sprememb + permission_commit_access: Dostop za predajo + permission_manage_boards: Uredi table + permission_view_messages: Poglej sporoÄila + permission_add_messages: Objavi sporoÄila + permission_edit_messages: Uredi sporoÄila + permission_edit_own_messages: Uredi lastna sporoÄila + permission_delete_messages: IzbriÅ¡i sporoÄila + permission_delete_own_messages: IzbriÅ¡i lastna sporoÄila + + project_module_issue_tracking: Sledenje zahtevkom + project_module_time_tracking: Sledenje Äasa + project_module_news: Novice + project_module_documents: Dokumenti + project_module_files: Datoteke + project_module_wiki: Wiki + project_module_repository: Shramba + project_module_boards: Table + + label_user: Uporabnik + label_user_plural: Uporabniki + label_user_new: Nov uporabnik + label_project: Projekt + label_project_new: Nov projekt + label_project_plural: Projekti + label_x_projects: + zero: ni projektov + one: 1 projekt + other: "%{count} projektov" + label_project_all: Vsi projekti + label_project_latest: Zadnji projekti + label_issue: Zahtevek + label_issue_new: Nov zahtevek + label_issue_plural: Zahtevki + label_issue_view_all: Poglej vse zahtevke + label_issues_by: "Zahtevki od %{value}" + label_issue_added: Zahtevek dodan + label_issue_updated: Zahtevek posodobljen + label_document: Dokument + label_document_new: Nov dokument + label_document_plural: Dokumenti + label_document_added: Dokument dodan + label_role: Vloga + label_role_plural: Vloge + label_role_new: Nova vloga + label_role_and_permissions: Vloge in dovoljenja + label_member: ÄŒlan + label_member_new: Nov Älan + label_member_plural: ÄŒlani + label_tracker: Vrsta zahtevka + label_tracker_plural: Vrste zahtevkov + label_tracker_new: Nova vrsta zahtevka + label_workflow: Potek dela + label_issue_status: Stanje zahtevka + label_issue_status_plural: Stanje zahtevkov + label_issue_status_new: Novo stanje + label_issue_category: Kategorija zahtevka + label_issue_category_plural: Kategorije zahtevkov + label_issue_category_new: Nova kategorija + label_custom_field: Polje po meri + label_custom_field_plural: Polja po meri + label_custom_field_new: Novo polje po meri + label_enumerations: Seznami + label_enumeration_new: Nova vrednost + label_information: Informacija + label_information_plural: Informacije + label_please_login: Prosimo prijavite se + label_register: Registracija + label_password_lost: Spremeni geslo + label_home: Domov + label_my_page: Moja stran + label_my_account: Moj raÄun + label_my_projects: Moji projekti + label_administration: Upravljanje + label_login: Prijava + label_logout: Odjava + label_help: PomoÄ + label_reported_issues: Prijavljeni zahtevki + label_assigned_to_me_issues: Zahtevki dodeljeni meni + label_last_login: Zadnja povezava + label_registered_on: Registriran + label_activity: Aktivnosti + label_overall_activity: Celotna aktivnost + label_user_activity: "Aktivnost %{value}" + label_new: Nov + label_logged_as: Prijavljen(a) kot + label_environment: Okolje + label_authentication: Overovitev + label_auth_source: NaÄin overovitve + label_auth_source_new: Nov naÄin overovitve + label_auth_source_plural: NaÄini overovitve + label_subproject_plural: Podprojekti + label_and_its_subprojects: "%{value} in njegovi podprojekti" + label_min_max_length: Min - Max dolžina + label_list: Seznam + label_date: Datum + label_integer: Celo Å¡tevilo + label_float: Decimalno Å¡tevilo + label_boolean: Boolean + label_string: Besedilo + label_text: Dolgo besedilo + label_attribute: Lastnost + label_attribute_plural: Lastnosti + label_no_data: Ni podatkov za prikaz + label_change_status: Spremeni stanje + label_history: Zgodovina + label_attachment: Datoteka + label_attachment_new: Nova datoteka + label_attachment_delete: IzbriÅ¡i datoteko + label_attachment_plural: Datoteke + label_file_added: Datoteka dodana + label_report: PoroÄilo + label_report_plural: PoroÄila + label_news: Novica + label_news_new: Dodaj novico + label_news_plural: Novice + label_news_latest: Zadnje novice + label_news_view_all: Poglej vse novice + label_news_added: Dodane novice + label_settings: Nastavitve + label_overview: Povzetek + label_version: Verzija + label_version_new: Nova verzija + label_version_plural: Verzije + label_confirmation: Potrditev + label_export_to: 'Na razpolago tudi v:' + label_read: Preberi... + label_public_projects: Javni projekti + label_open_issues: odprt zahtevek + label_open_issues_plural: odprti zahtevki + label_closed_issues: zaprt zahtevek + label_closed_issues_plural: zaprti zahtevki + label_x_open_issues_abbr_on_total: + zero: 0 odprtih / %{total} + one: 1 odprt / %{total} + other: "%{count} odprtih / %{total}" + label_x_open_issues_abbr: + zero: 0 odprtih + one: 1 odprt + other: "%{count} odprtih" + label_x_closed_issues_abbr: + zero: 0 zaprtih + one: 1 zaprt + other: "%{count} zaprtih" + label_total: Skupaj + label_permissions: Dovoljenja + label_current_status: Trenutno stanje + label_new_statuses_allowed: Novi zahtevki dovoljeni + label_all: vsi + label_none: noben + label_nobody: nihÄe + label_next: Naslednji + label_previous: PrejÅ¡nji + label_used_by: V uporabi od + label_details: Podrobnosti + label_add_note: Dodaj zabeležko + label_per_page: Na stran + label_calendar: Koledar + label_months_from: mesecev od + label_gantt: Gantogram + label_internal: Notranji + label_last_changes: "zadnjih %{count} sprememb" + label_change_view_all: Poglej vse spremembe + label_personalize_page: Individualiziraj to stran + label_comment: Komentar + label_comment_plural: Komentarji + label_x_comments: + zero: ni komentarjev + one: 1 komentar + other: "%{count} komentarjev" + label_comment_add: Dodaj komentar + label_comment_added: Komentar dodan + label_comment_delete: IzbriÅ¡i komentarje + label_query: Iskanje po meri + label_query_plural: Iskanja po meri + label_query_new: Novo iskanje + label_filter_add: Dodaj filter + label_filter_plural: Filtri + label_equals: je enako + label_not_equals: ni enako + label_in_less_than: v manj kot + label_in_more_than: v veÄ kot + label_in: v + label_today: danes + label_all_time: v vsem Äasu + label_yesterday: vÄeraj + label_this_week: ta teden + label_last_week: pretekli teden + label_last_n_days: "zadnjih %{count} dni" + label_this_month: ta mesec + label_last_month: zadnji mesec + label_this_year: to leto + label_date_range: Razpon datumov + label_less_than_ago: manj kot dni nazaj + label_more_than_ago: veÄ kot dni nazaj + label_ago: dni nazaj + label_contains: vsebuje + label_not_contains: ne vsebuje + label_day_plural: dni + label_repository: Shramba + label_repository_plural: Shrambe + label_browse: Prebrskaj + label_revision: Revizija + label_revision_plural: Revizije + label_associated_revisions: Povezane revizije + label_added: dodano + label_modified: spremenjeno + label_copied: kopirano + label_renamed: preimenovano + label_deleted: izbrisano + label_latest_revision: Zadnja revizija + label_latest_revision_plural: Zadnje revizije + label_view_revisions: Poglej revizije + label_max_size: NajveÄja velikost + label_sort_highest: Premakni na vrh + label_sort_higher: Premakni gor + label_sort_lower: Premakni dol + label_sort_lowest: Premakni na dno + label_roadmap: NaÄrt + label_roadmap_due_in: "Do %{value}" + label_roadmap_overdue: "%{value} zakasnel" + label_roadmap_no_issues: Ni zahtevkov za to verzijo + label_search: IÅ¡Äi + label_result_plural: Rezultati + label_all_words: Vse besede + label_wiki: Predstavitev + label_wiki_edit: Uredi stran + label_wiki_edit_plural: Uredi strani + label_wiki_page: Predstavitvena stran + label_wiki_page_plural: Predstavitvene strani + label_index_by_title: Razvrsti po naslovu + label_index_by_date: Razvrsti po datumu + label_current_version: Trenutna verzija + label_preview: Predogled + label_feed_plural: Atom viri + label_changes_details: Podrobnosti o vseh spremembah + label_issue_tracking: Sledenje zahtevkom + label_spent_time: Porabljen Äas + label_f_hour: "%{value} ura" + label_f_hour_plural: "%{value} ur" + label_time_tracking: Sledenje Äasu + label_change_plural: Spremembe + label_statistics: Statistika + label_commits_per_month: Predaj na mesec + label_commits_per_author: Predaj na avtorja + label_view_diff: Preglej razlike + label_diff_inline: znotraj + label_diff_side_by_side: vzporedno + label_options: Možnosti + label_copy_workflow_from: Kopiraj potek dela od + label_permissions_report: PoroÄilo o dovoljenjih + label_watched_issues: Spremljani zahtevki + label_related_issues: Povezani zahtevki + label_applied_status: Uveljavljeno stanje + label_loading: Nalaganje... + label_relation_new: Nova povezava + label_relation_delete: IzbriÅ¡i povezavo + label_relates_to: povezan z + label_duplicates: duplikati + label_duplicated_by: dupliciral + label_blocks: blok + label_blocked_by: blokiral + label_precedes: ima prednost pred + label_follows: sledi + label_end_to_start: konec na zaÄetek + label_end_to_end: konec na konec + label_start_to_start: zaÄetek na zaÄetek + label_start_to_end: zaÄetek na konec + label_stay_logged_in: Ostani prijavljen(a) + label_disabled: onemogoÄi + label_show_completed_versions: Prikaži zakljuÄene verzije + label_me: jaz + label_board: Forum + label_board_new: Nov forum + label_board_plural: Forumi + label_topic_plural: Teme + label_message_plural: SporoÄila + label_message_last: Zadnje sporoÄilo + label_message_new: Novo sporoÄilo + label_message_posted: SporoÄilo dodano + label_reply_plural: Odgovori + label_send_information: PoÅ¡lji informacijo o raÄunu uporabniku + label_year: Leto + label_month: Mesec + label_week: Teden + label_date_from: Do + label_date_to: Do + label_language_based: Glede na uporabnikov jezik + label_sort_by: "Razporedi po %{value}" + label_send_test_email: PoÅ¡lji testno e-pismo + label_feeds_access_key_created_on: "Atom dostopni kljuÄ narejen %{value} nazaj" + label_module_plural: Moduli + label_added_time_by: "Dodal %{author} %{age} nazaj" + label_updated_time_by: "Posodobil %{author} %{age} nazaj" + label_updated_time: "Posodobljeno %{value} nazaj" + label_jump_to_a_project: SkoÄi na projekt... + label_file_plural: Datoteke + label_changeset_plural: Zapisi sprememb + label_default_columns: Privzeti stolpci + label_no_change_option: (Ni spremembe) + label_bulk_edit_selected_issues: Uredi izbrane zahtevke skupaj + label_theme: Tema + label_default: Privzeto + label_search_titles_only: PreiÅ¡Äi samo naslove + label_user_mail_option_all: "Za vsak dogodek v vseh mojih projektih" + label_user_mail_option_selected: "Za vsak dogodek samo na izbranih projektih..." + label_user_mail_no_self_notified: "Ne želim biti opozorjen(a) na spremembe, ki jih naredim sam(a)" + label_registration_activation_by_email: aktivacija raÄuna po e-poÅ¡ti + label_registration_manual_activation: roÄna aktivacija raÄuna + label_registration_automatic_activation: samodejna aktivacija raÄuna + label_display_per_page: "Na stran: %{value}" + label_age: Starost + label_change_properties: Sprememba lastnosti + label_general: SploÅ¡no + label_more: VeÄ + label_scm: SCM + label_plugins: VtiÄniki + label_ldap_authentication: LDAP overovljanje + label_downloads_abbr: D/L + label_optional_description: Neobvezen opis + label_add_another_file: Dodaj Å¡e eno datoteko + label_preferences: Preference + label_chronological_order: KronoloÅ¡ko + label_reverse_chronological_order: Obrnjeno kronoloÅ¡ko + label_planning: NaÄrtovanje + label_incoming_emails: PrihajajoÄa e-poÅ¡ta + label_generate_key: Ustvari kljuÄ + label_issue_watchers: Spremljevalci + label_example: Vzorec + + button_login: Prijavi se + button_submit: PoÅ¡lji + button_save: Shrani + button_check_all: OznaÄi vse + button_uncheck_all: OdznaÄi vse + button_delete: IzbriÅ¡i + button_create: Ustvari + button_test: Testiraj + button_edit: Uredi + button_add: Dodaj + button_change: Spremeni + button_apply: Uporabi + button_clear: PoÄisti + button_lock: Zakleni + button_unlock: Odkleni + button_download: Prenesi + button_list: Seznam + button_view: Pogled + button_move: Premakni + button_back: Nazaj + button_cancel: PrekliÄi + button_activate: Aktiviraj + button_sort: Razvrsti + button_log_time: Beleži Äas + button_rollback: Povrni na to verzijo + button_watch: Spremljaj + button_unwatch: Ne spremljaj + button_reply: Odgovori + button_archive: Arhiviraj + button_unarchive: Odarhiviraj + button_reset: Ponastavi + button_rename: Preimenuj + button_change_password: Spremeni geslo + button_copy: Kopiraj + button_annotate: ZapiÅ¡i pripombo + button_update: Posodobi + button_configure: Konfiguriraj + button_quote: Citiraj + + status_active: aktivni + status_registered: registriran + status_locked: zaklenjen + + text_select_mail_notifications: Izberi dejanja za katera naj bodo poslana oznanila preko e-poÅ¡to. + text_regexp_info: npr. ^[A-Z0-9]+$ + text_min_max_length_info: 0 pomeni brez omejitev + text_project_destroy_confirmation: Ali ste prepriÄani da želite izbrisati izbrani projekt in vse z njim povezane podatke? + text_subprojects_destroy_warning: "Njegov(i) podprojekt(i): %{value} bodo prav tako izbrisani." + text_workflow_edit: Izberite vlogo in zahtevek za urejanje poteka dela + text_are_you_sure: Ali ste prepriÄani? + text_tip_issue_begin_day: naloga z zaÄetkom na ta dan + text_tip_issue_end_day: naloga z zakljuÄkom na ta dan + text_tip_issue_begin_end_day: naloga ki se zaÄne in konÄa ta dan + text_caracters_maximum: "najveÄ %{count} znakov." + text_caracters_minimum: "Mora biti vsaj dolg vsaj %{count} znakov." + text_length_between: "Dolžina med %{min} in %{max} znakov." + text_tracker_no_workflow: Potek dela za to vrsto zahtevka ni doloÄen + text_unallowed_characters: Nedovoljeni znaki + text_comma_separated: Dovoljenih je veÄ vrednosti (loÄenih z vejico). + text_issues_ref_in_commit_messages: Zahtevki sklicev in popravkov v sporoÄilu predaje + text_issue_added: "Zahtevek %{id} je sporoÄil(a) %{author}." + text_issue_updated: "Zahtevek %{id} je posodobil(a) %{author}." + text_wiki_destroy_confirmation: Ali ste prepriÄani da želite izbrisati to wiki stran in vso njeno vsebino? + text_issue_category_destroy_question: "Nekateri zahtevki (%{count}) so dodeljeni tej kategoriji. Kaj želite storiti?" + text_issue_category_destroy_assignments: Odstrani naloge v kategoriji + text_issue_category_reassign_to: Ponovno dodeli zahtevke tej kategoriji + text_user_mail_option: "Na neizbrane projekte boste prejemali le obvestila o zadevah ki jih spremljate ali v katere ste vkljuÄeni (npr. zahtevki katerih avtor(ica) ste)" + text_no_configuration_data: "Vloge, vrste zahtevkov, statusi zahtevkov in potek dela Å¡e niso bili doloÄeni. \nZelo priporoÄljivo je, da naložite privzeto konfiguracijo, ki jo lahko kasneje tudi prilagodite." + text_load_default_configuration: Naloži privzeto konfiguracijo + text_status_changed_by_changeset: "Dodano v zapis sprememb %{value}." + text_issues_destroy_confirmation: 'Ali ste prepriÄani, da želite izbrisati izbrani(e) zahtevek(ke)?' + text_select_project_modules: 'Izberite module, ki jih želite omogoÄiti za ta projekt:' + text_default_administrator_account_changed: Spremenjen privzeti administratorski raÄun + text_file_repository_writable: OmogoÄeno pisanje v shrambo datotek + text_rmagick_available: RMagick je na voljo(neobvezno) + text_destroy_time_entries_question: "%{hours} ur je bilo opravljenih na zahtevku, ki ga želite izbrisati. Kaj želite storiti?" + text_destroy_time_entries: IzbriÅ¡i opravljene ure + text_assign_time_entries_to_project: Predaj opravljene ure projektu + text_reassign_time_entries: 'Prenesi opravljene ure na ta zahtevek:' + text_user_wrote: "%{value} je napisal(a):" + text_enumeration_destroy_question: "%{count} objektov je doloÄenih tej vrednosti." + text_enumeration_category_reassign_to: 'Ponastavi jih na to vrednost:' + text_email_delivery_not_configured: "E-poÅ¡tna dostava ni nastavljena in oznanila so onemogoÄena.\nNastavite vaÅ¡ SMTP strežnik v config/configuration.yml in ponovno zaženite aplikacijo da ga omogoÄite.\n" + text_repository_usernames_mapping: "Izberite ali posodobite Redmine uporabnika dodeljenega vsakemu uporabniÅ¡kemu imenu najdenemu v zapisniku shrambe.\n Uporabniki z enakim Redmine ali shrambinem uporabniÅ¡kem imenu ali e-poÅ¡tnem naslovu so samodejno dodeljeni." + text_diff_truncated: '... Ta sprememba je bila odsekana ker presega najveÄjo velikost ki je lahko prikazana.' + + default_role_manager: Upravnik + default_role_developer: Razvijalec + default_role_reporter: PoroÄevalec + default_tracker_bug: HroÅ¡Ä + default_tracker_feature: Funkcija + default_tracker_support: Podpora + default_issue_status_new: Nov + default_issue_status_in_progress: V teku + default_issue_status_resolved: ReÅ¡en + default_issue_status_feedback: Povratna informacija + default_issue_status_closed: ZakljuÄen + default_issue_status_rejected: Zavrnjen + default_doc_category_user: UporabniÅ¡ka dokumentacija + default_doc_category_tech: TehniÄna dokumentacija + default_priority_low: Nizka + default_priority_normal: ObiÄajna + default_priority_high: Visoka + default_priority_urgent: Urgentna + default_priority_immediate: TakojÅ¡nje ukrepanje + default_activity_design: Oblikovanje + default_activity_development: Razvoj + + enumeration_issue_priorities: Prioritete zahtevkov + enumeration_doc_categories: Kategorije dokumentov + enumeration_activities: Aktivnosti (sledenje Äasa) + warning_attachments_not_saved: "%{count} datotek(e) ni bilo mogoÄe shraniti." + field_editable: Uredljivo + text_plugin_assets_writable: Zapisljiva mapa za vtiÄnike + label_display: Prikaz + button_create_and_continue: Ustvari in nadaljuj + text_custom_field_possible_values_info: 'Ena vrstica za vsako vrednost' + setting_repository_log_display_limit: NajveÄje Å¡tevilo prikazanih revizij v log datoteki + setting_file_max_size_displayed: NajveÄja velikost besedilnih datotek v vkljuÄenem prikazu + field_watcher: Opazovalec + setting_openid: Dovoli OpenID prijavo in registracijo + field_identity_url: OpenID URL + label_login_with_open_id_option: ali se prijavi z OpenID + field_content: Vsebina + label_descending: PadajoÄe + label_sort: Razvrsti + label_ascending: NaraÅ¡ÄajoÄe + label_date_from_to: Od %{start} do %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Ta stran ima %{descendants} podstran(i) in naslednik(ov). Kaj želite storiti? + text_wiki_page_reassign_children: Znova dodeli podstrani tej glavni strani + text_wiki_page_nullify_children: Obdrži podstrani kot glavne strani + text_wiki_page_destroy_children: IzbriÅ¡i podstrani in vse njihove naslednike + setting_password_min_length: Minimalna dolžina gesla + field_group_by: Združi rezultate po + mail_subject_wiki_content_updated: "'%{id}' wiki stran je bila posodobljena" + label_wiki_content_added: Wiki stran dodana + mail_subject_wiki_content_added: "'%{id}' wiki stran je bila dodana" + mail_body_wiki_content_added: "%{author} je dodal '%{id}' wiki stran" + label_wiki_content_updated: Wiki stran posodobljena + mail_body_wiki_content_updated: "%{author} je posodobil '%{id}' wiki stran." + permission_add_project: Ustvari projekt + setting_new_project_user_role_id: Vloga, dodeljena neadministratorskemu uporabniku, ki je ustvaril projekt + label_view_all_revisions: Poglej vse revizije + label_tag: Oznaka + label_branch: Veja + error_no_tracker_in_project: Noben sledilnik ni povezan s tem projektom. Prosimo preverite nastavitve projekta. + error_no_default_issue_status: Privzeti zahtevek ni definiran. Prosimo preverite svoje nastavitve (Pojdite na "Administracija -> Stanje zahtevkov"). + text_journal_changed: "%{label} se je spremenilo iz %{old} v %{new}" + text_journal_set_to: "%{label} nastavljeno na %{value}" + text_journal_deleted: "%{label} izbrisan (%{old})" + label_group_plural: Skupine + label_group: Skupina + label_group_new: Nova skupina + label_time_entry_plural: Porabljen Äas + text_journal_added: "%{label} %{value} dodan" + field_active: Aktiven + enumeration_system_activity: Sistemska aktivnost + permission_delete_issue_watchers: IzbriÅ¡i opazovalce + version_status_closed: zaprt + version_status_locked: zaklenjen + version_status_open: odprt + error_can_not_reopen_issue_on_closed_version: Zahtevek dodeljen zaprti verziji ne more biti ponovno odprt + label_user_anonymous: Anonimni + button_move_and_follow: Premakni in sledi + setting_default_projects_modules: Privzeti moduli za nove projekte + setting_gravatar_default: Privzeta Gravatar slika + field_sharing: Deljenje + label_version_sharing_hierarchy: S projektno hierarhijo + label_version_sharing_system: Z vsemi projekti + label_version_sharing_descendants: S podprojekti + label_version_sharing_tree: Z drevesom projekta + label_version_sharing_none: Ni deljeno + error_can_not_archive_project: Ta projekt ne more biti arhiviran + button_duplicate: Podvoji + button_copy_and_follow: Kopiraj in sledi + label_copy_source: Vir + setting_issue_done_ratio: IzraÄunaj razmerje opravljenega zahtevka z + setting_issue_done_ratio_issue_status: Uporabi stanje zahtevka + error_issue_done_ratios_not_updated: Razmerje opravljenega zahtevka ni bilo posodobljeno. + error_workflow_copy_target: Prosimo izberite ciljni(e) sledilnik(e) in vlogo(e) + setting_issue_done_ratio_issue_field: Uporabi polje zahtevka + label_copy_same_as_target: Enako kot cilj + label_copy_target: Cilj + notice_issue_done_ratios_updated: Razmerje opravljenega zahtevka posodobljeno. + error_workflow_copy_source: Prosimo izberite vir zahtevka ali vlogo + label_update_issue_done_ratios: Posodobi razmerje opravljenega zahtevka + setting_start_of_week: ZaÄni koledarje z + permission_view_issues: Poglej zahtevke + label_display_used_statuses_only: Prikaži samo stanja ki uporabljajo ta sledilnik + label_revision_id: Revizija %{value} + label_api_access_key: API dostopni kljuÄ + label_api_access_key_created_on: API dostopni kljuÄ ustvarjen pred %{value} + label_feeds_access_key: Atom dostopni kljuÄ + notice_api_access_key_reseted: VaÅ¡ API dostopni kljuÄ je bil ponastavljen. + setting_rest_api_enabled: OmogoÄi REST spletni servis + label_missing_api_access_key: ManjkajoÄ API dostopni kljuÄ + label_missing_feeds_access_key: ManjkajoÄ Atom dostopni kljuÄ + button_show: Prikaži + text_line_separated: Dovoljenih veÄ vrednosti (ena vrstica za vsako vrednost). + setting_mail_handler_body_delimiters: Odreži e-poÅ¡to po eni od teh vrstic + permission_add_subprojects: Ustvari podprojekte + label_subproject_new: Nov podprojekt + text_own_membership_delete_confirmation: |- + Odstranili boste nekatere ali vse od dovoljenj zaradi Äesar morda ne boste mogli veÄ urejati tega projekta. + Ali ste prepriÄani, da želite nadaljevati? + label_close_versions: Zapri dokonÄane verzije + label_board_sticky: Lepljivo + label_board_locked: Zaklenjeno + permission_export_wiki_pages: Izvozi wiki strani + setting_cache_formatted_text: Predpomni oblikovano besedilo + permission_manage_project_activities: Uredi aktivnosti projekta + error_unable_delete_issue_status: Stanja zahtevka ni bilo možno spremeniti + label_profile: Profil + permission_manage_subtasks: Uredi podnaloge + field_parent_issue: Nadrejena naloga + label_subtask_plural: Podnaloge + label_project_copy_notifications: Med kopiranjem projekta poÅ¡lji e-poÅ¡tno sporoÄilo + error_can_not_delete_custom_field: Polja po meri ni mogoÄe izbrisati + error_unable_to_connect: Povezava ni mogoÄa (%{value}) + error_can_not_remove_role: Ta vloga je v uporabi in je ni mogoÄe izbrisati. + error_can_not_delete_tracker: Ta sledilnik vsebuje zahtevke in se ga ne more izbrisati. + field_principal: Upravnik varnosti + label_my_page_block: Moj gradnik strani + notice_failed_to_save_members: "Shranjevanje uporabnika(ov) ni uspelo: %{errors}." + text_zoom_out: Približaj + text_zoom_in: Oddalji + notice_unable_delete_time_entry: Brisanje dnevnika porabljenaga Äasa ni mogoÄe. + label_overall_spent_time: Skupni porabljeni Äas + field_time_entries: Beleži porabljeni Äas + project_module_gantt: Gantogram + project_module_calendar: Koledear + button_edit_associated_wikipage: "Uredi povezano Wiki stran: %{page_title}" + field_text: Besedilno polje + label_user_mail_option_only_owner: Samo za stvari katerih lastnik sem + setting_default_notification_option: Privzeta možnost obveÅ¡Äanja + label_user_mail_option_only_my_events: Samo za stvari, ki jih opazujem ali sem v njih vpleten + label_user_mail_option_only_assigned: Samo za stvari, ki smo mi dodeljene + label_user_mail_option_none: Noben dogodek + field_member_of_group: PooblaÅ¡ÄenÄeva skupina + field_assigned_to_role: PooblaÅ¡ÄenÄeva vloga + notice_not_authorized_archived_project: Projekt, do katerega poskuÅ¡ate dostopati, je bil arhiviran. + label_principal_search: "PoiÅ¡Äi uporabnika ali skupino:" + label_user_search: "PoiÅ¡Äi uporabnikia:" + field_visible: Viden + setting_emails_header: Glava e-poÅ¡te + setting_commit_logtime_activity_id: Aktivnost zabeleženega Äasa + text_time_logged_by_changeset: Uporabljeno v spremembi %{value}. + setting_commit_logtime_enabled: OmogoÄi beleženje Äasa + notice_gantt_chart_truncated: Graf je bil odrezan, ker je prekoraÄil najveÄje dovoljeno Å¡tevilo elementov, ki se jih lahko prikaže (%{max}) + setting_gantt_items_limit: NajveÄje Å¡tevilo elementov prikazano na gantogramu + field_warn_on_leaving_unsaved: Opozori me, kadar zapuÅ¡Äam stran z neshranjenim besedilom + text_warn_on_leaving_unsaved: Trenutna stran vsebuje neshranjeno besedilo ki bo izgubljeno, Äe zapustite to stran. + label_my_queries: Moje poizvedbe po meri + text_journal_changed_no_detail: "%{label} posodobljen" + label_news_comment_added: Komentar dodan novici + button_expand_all: RazÅ¡iri vse + button_collapse_all: SkrÄi vse + label_additional_workflow_transitions_for_assignee: Dovoljeni dodatni prehodi kadar je uporabnik pooblaÅ¡Äenec + label_additional_workflow_transitions_for_author: Dovoljeni dodatni prehodi kadar je uporabnik avtor + label_bulk_edit_selected_time_entries: Skupinsko urejanje izbranih Äasovnih zapisov + text_time_entries_destroy_confirmation: Ali ste prepriÄani, da želite izbristai izbran(e) Äasovn(i/e) zapis(e)? + label_role_anonymous: Anonimni + label_role_non_member: NeÄlan + label_issue_note_added: Dodan zaznamek + label_issue_status_updated: Status posodobljen + label_issue_priority_updated: Prioriteta posodobljena + label_issues_visibility_own: Zahtevek ustvarjen s strani uporabnika ali dodeljen uporabniku + field_issues_visibility: Vidljivost zahtevkov + label_issues_visibility_all: Vsi zahtevki + permission_set_own_issues_private: Nastavi lastne zahtevke kot javne ali zasebne + field_is_private: Zaseben + permission_set_issues_private: Nastavi zahtevke kot javne ali zasebne + label_issues_visibility_public: Vsi nezasebni zahtevki + text_issues_destroy_descendants_confirmation: To bo izbrisalo tudi %{count} podnalog(o). + field_commit_logs_encoding: Kodiranje sporoÄil ob predaji + field_scm_path_encoding: Pot do kodiranja + text_scm_path_encoding_note: "Privzeto: UTF-8" + field_path_to_repository: Pot do shrambe + field_root_directory: Korenska mapa + field_cvs_module: Modul + field_cvsroot: CVSROOT + text_mercurial_repository_note: Lokalna shramba (npr. /hgrepo, c:\hgrepo) + text_scm_command: Ukaz + text_scm_command_version: Verzija + label_git_report_last_commit: SporoÄi zadnje uveljavljanje datotek in map + text_scm_config: Svoje SCM ukaze lahko nastavite v datoteki config/configuration.yml. Po urejanju prosimo ponovno zaženite aplikacijo. + text_scm_command_not_available: SCM ukaz ni na voljo. Prosimo preverite nastavitve v upravljalskem podoknu. + + text_git_repository_note: Shramba je prazna in lokalna (npr. /gitrepo, c:\gitrepo) + + notice_issue_successful_create: Ustvarjen zahtevek %{id}. + label_between: med + setting_issue_group_assignment: Dovoli dodeljevanje zahtevka skupinam + label_diff: diff + + 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 zahtevek + one: 1 zahtevek + other: "%{count} zahtevki" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: vsi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: S podprojekti + label_cross_project_tree: Z drevesom projekta + label_cross_project_hierarchy: S projektno hierarhijo + label_cross_project_system: Z vsemi projekti + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Skupaj + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/1611b5791962624ae0d84516d78fd057b4a95ed3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/16/1611b5791962624ae0d84516d78fd057b4a95ed3.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,136 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 GroupTest < ActiveSupport::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, + :projects_trackers, + :roles, + :member_roles, + :members, + :groups_users + + include Redmine::I18n + + def test_create + g = Group.new(:name => 'New group') + assert g.save + g.reload + assert_equal 'New group', g.name + end + + def test_name_should_accept_255_characters + name = 'a' * 255 + g = Group.new(:name => name) + assert g.save + g.reload + assert_equal name, g.name + end + + def test_blank_name_error_message + set_language_if_valid 'en' + g = Group.new + assert !g.save + assert_include "Name can't be blank", g.errors.full_messages + end + + def test_blank_name_error_message_fr + set_language_if_valid 'fr' + str = "Nom doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + g = Group.new + assert !g.save + assert_include str, g.errors.full_messages + end + + def test_group_roles_should_be_given_to_added_user + group = Group.find(11) + user = User.find(9) + project = Project.first + + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.users << user + assert user.member_of?(project) + end + + def test_new_roles_should_be_given_to_existing_user + group = Group.find(11) + user = User.find(9) + project = Project.first + + group.users << user + m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + assert user.member_of?(project) + end + + def test_user_roles_should_updated_when_updating_user_ids + group = Group.find(11) + user = User.find(9) + project = Project.first + + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.user_ids = [user.id] + group.save! + assert User.find(9).member_of?(project) + + group.user_ids = [1] + group.save! + assert !User.find(9).member_of?(project) + end + + def test_user_roles_should_updated_when_updating_group_roles + group = Group.find(11) + user = User.find(9) + project = Project.first + group.users << user + m = Member.create!(:principal => group, :project => project, :role_ids => [1]) + assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [1, 2] + assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [2] + assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [1] + assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort + end + + def test_user_memberships_should_be_removed_when_removing_group_membership + assert User.find(8).member_of?(Project.find(5)) + Member.find_by_project_id_and_user_id(5, 10).destroy + assert !User.find(8).member_of?(Project.find(5)) + end + + def test_user_roles_should_be_removed_when_removing_user_from_group + assert User.find(8).member_of?(Project.find(5)) + User.find(8).groups = [] + assert !User.find(8).member_of?(Project.find(5)) + end + + def test_destroy_should_unassign_issues + group = Group.first + Issue.update_all(["assigned_to_id = ?", group.id], 'id = 1') + + assert group.destroy + assert group.destroyed? + + assert_equal nil, Issue.find(1).assigned_to_id + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/1611dde1fb0ec5f844a52d9ac947a7c023328e24.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/16/1611dde1fb0ec5f844a52d9ac947a7c023328e24.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,38 @@ +# Redmine runs tests on own continuous integration server. +# http://www.redmine.org/projects/redmine/wiki/Continuous_integration +# You can also run tests on your environment. +language: ruby +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - 2.0 + - jruby +matrix: + allow_failures: + # SCM tests fail randomly due to IO.popen(). + # https://github.com/jruby/jruby/issues/779 + - rvm: jruby +env: + - "TEST_SUITE=units DATABASE_ADAPTER=postgresql" + - "TEST_SUITE=functionals DATABASE_ADAPTER=postgresql" + - "TEST_SUITE=integration DATABASE_ADAPTER=postgresql" + - "TEST_SUITE=units DATABASE_ADAPTER=mysql" + - "TEST_SUITE=functionals DATABASE_ADAPTER=mysql" + - "TEST_SUITE=integration DATABASE_ADAPTER=mysql" + - "TEST_SUITE=units DATABASE_ADAPTER=sqlite3" + - "TEST_SUITE=functionals DATABASE_ADAPTER=sqlite3" + - "TEST_SUITE=integration DATABASE_ADAPTER=sqlite3" +before_install: + - "sudo apt-get update -qq" + - "sudo apt-get --no-install-recommends install bzr cvs git mercurial subversion" +script: + - "SCMS=bazaar,cvs,subversion,git,mercurial,filesystem" + - "export SCMS" + - "git --version" + - "bundle install" + - "RUN_ON_NOT_OFFICIAL='' RUBY_VER=1.9 BRANCH=trunk bundle exec rake config/database.yml" + - "bundle install" + - "JRUBY_OPTS=-J-Xmx1024m bundle exec rake ci" +notifications: + email: false diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/164226a6305a1cc9df96d889179a566dfaf3eb26.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/16/164226a6305a1cc9df96d889179a566dfaf3eb26.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,98 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 MimeType + + MIME_TYPES = { + 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade', + 'text/css' => 'css', + 'text/html' => 'html,htm,xhtml', + 'text/jsp' => 'jsp', + 'text/x-c' => 'c,cpp,cc,h,hh', + 'text/x-csharp' => 'cs', + 'text/x-java' => 'java', + 'text/x-html-template' => 'rhtml', + 'text/x-perl' => 'pl,pm', + 'text/x-php' => 'php,php3,php4,php5', + 'text/x-python' => 'py', + 'text/x-ruby' => 'rb,rbw,ruby,rake,erb', + 'text/x-csh' => 'csh', + 'text/x-sh' => 'sh', + 'text/xml' => 'xml,xsd,mxml', + 'text/yaml' => 'yml,yaml', + 'text/csv' => 'csv', + 'text/x-po' => 'po', + 'image/gif' => 'gif', + 'image/jpeg' => 'jpg,jpeg,jpe', + 'image/png' => 'png', + 'image/tiff' => 'tiff,tif', + 'image/x-ms-bmp' => 'bmp', + 'image/x-xpixmap' => 'xpm', + 'image/svg+xml'=> 'svg', + 'application/javascript' => 'js', + 'application/pdf' => 'pdf', + 'application/rtf' => 'rtf', + 'application/msword' => 'doc', + 'application/vnd.ms-excel' => 'xls', + 'application/vnd.ms-powerpoint' => 'ppt,pps', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx', + 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', + 'application/vnd.oasis.opendocument.text' => 'odt', + 'application/vnd.oasis.opendocument.presentation' => 'odp', + 'application/x-7z-compressed' => '7z', + 'application/x-rar-compressed' => 'rar', + 'application/x-tar' => 'tar', + 'application/zip' => 'zip', + 'application/x-gzip' => 'gz', + }.freeze + + EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)| + exts.split(',').each {|ext| map[ext.strip] = type} + map + end + + # returns mime type for name or nil if unknown + def self.of(name) + return nil unless name + m = name.to_s.match(/(^|\.)([^\.]+)$/) + EXTENSIONS[m[2].downcase] if m + end + + # Returns the css class associated to + # the mime type of name + def self.css_class_of(name) + mime = of(name) + mime && mime.gsub('/', '-') + end + + def self.main_mimetype_of(name) + mimetype = of(name) + mimetype.split('/').first if mimetype + end + + # return true if mime-type for name is type/* + # otherwise false + def self.is_type?(type, name) + main_mimetype = main_mimetype_of(name) + type.to_s == main_mimetype + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/167405f8fc306f6d744a19a6721080ccc76c8dbc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/16/167405f8fc306f6d744a19a6721080ccc76c8dbc.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,542 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 MailHandler < ActionMailer::Base + include ActionView::Helpers::SanitizeHelper + include Redmine::I18n + + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end + + attr_reader :email, :user + + def self.receive(email, options={}) + @@handler_options = options.dup + + @@handler_options[:issue] ||= {} + + if @@handler_options[:allow_override].is_a?(String) + @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) + end + @@handler_options[:allow_override] ||= [] + # Project needs to be overridable if not specified + @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) + # Status overridable by default + @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) + + @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1') + @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1') + @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1') + + email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding) + super(email) + end + + # Extracts MailHandler options from environment variables + # Use when receiving emails with rake tasks + def self.extract_options_from_env(env) + options = {:issue => {}} + %w(project status tracker category priority).each do |option| + options[:issue][option.to_sym] = env[option] if env[option] + end + %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option| + options[option.to_sym] = env[option] if env[option] + end + options + end + + def logger + Rails.logger + end + + cattr_accessor :ignored_emails_headers + @@ignored_emails_headers = { + 'X-Auto-Response-Suppress' => 'oof', + 'Auto-Submitted' => /^auto-/ + } + + # Processes incoming emails + # Returns the created object (eg. an issue, a message) or false + def receive(email) + @email = email + sender_email = email.from.to_a.first.to_s.strip + # Ignore emails received from the application emission address to avoid hell cycles + if sender_email.downcase == Setting.mail_from.to_s.strip.downcase + if logger + logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" + end + return false + end + # Ignore auto generated emails + self.class.ignored_emails_headers.each do |key, ignored_value| + value = email.header[key] + if value + value = value.to_s.downcase + if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value + if logger + logger.info "MailHandler: ignoring email with #{key}:#{value} header" + end + return false + end + end + end + @user = User.find_by_mail(sender_email) if sender_email.present? + if @user && !@user.active? + if logger + logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" + end + return false + end + if @user.nil? + # Email was submitted by an unknown user + case @@handler_options[:unknown_user] + when 'accept' + @user = User.anonymous + when 'create' + @user = create_user_from_email + if @user + if logger + logger.info "MailHandler: [#{@user.login}] account created" + end + add_user_to_group(@@handler_options[:default_group]) + unless @@handler_options[:no_account_notice] + Mailer.account_information(@user, @user.password).deliver + end + else + if logger + logger.error "MailHandler: could not create account for [#{sender_email}]" + end + return false + end + else + # Default behaviour, emails from unknown users are ignored + if logger + logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" + end + return false + end + end + User.current = @user + dispatch + end + + private + + MESSAGE_ID_RE = %r{^ e + # TODO: send a email to the user + logger.error e.message if logger + false + rescue MissingInformation => e + logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "MailHandler: unauthorized attempt from #{user}" if logger + false + end + + def dispatch_to_default + receive_issue + end + + # Creates a new issue + def receive_issue + project = target_project + # check permission + unless @@handler_options[:no_permission_check] + raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) + end + + issue = Issue.new(:author => user, :project => project) + issue.safe_attributes = issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + issue.subject = cleaned_up_subject + if issue.subject.blank? + issue.subject = '(no subject)' + end + issue.description = cleaned_up_text_body + issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date? + + # add To and Cc as watchers before saving so the watchers can reply to Redmine + add_watchers(issue) + issue.save! + add_attachments(issue) + logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger + issue + end + + # Adds a note to an existing issue + def receive_issue_reply(issue_id, from_journal=nil) + issue = Issue.find_by_id(issue_id) + return unless issue + # check permission + unless @@handler_options[:no_permission_check] + unless user.allowed_to?(:add_issue_notes, issue.project) || + user.allowed_to?(:edit_issues, issue.project) + raise UnauthorizedAction + end + end + + # ignore CLI-supplied defaults for new issues + @@handler_options[:issue].clear + + journal = issue.init_journal(user) + if from_journal && from_journal.private_notes? + # If the received email was a reply to a private note, make the added note private + issue.private_notes = true + end + issue.safe_attributes = issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + journal.notes = cleaned_up_text_body + add_attachments(issue) + issue.save! + if logger + logger.info "MailHandler: issue ##{issue.id} updated by #{user}" + end + journal + end + + # Reply will be added to the issue + def receive_journal_reply(journal_id) + journal = Journal.find_by_id(journal_id) + if journal && journal.journalized_type == 'Issue' + receive_issue_reply(journal.journalized_id, journal) + end + end + + # Receives a reply to a forum message + def receive_message_reply(message_id) + message = Message.find_by_id(message_id) + if message + message = message.root + + unless @@handler_options[:no_permission_check] + raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) + end + + if !message.locked? + reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, + :content => cleaned_up_text_body) + reply.author = user + reply.board = message.board + message.children << reply + add_attachments(reply) + reply + else + if logger + logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" + end + end + end + end + + def add_attachments(obj) + if email.attachments && email.attachments.any? + email.attachments.each do |attachment| + next unless accept_attachment?(attachment) + obj.attachments << Attachment.create(:container => obj, + :file => attachment.decoded, + :filename => attachment.filename, + :author => user, + :content_type => attachment.mime_type) + end + end + end + + # Returns false if the +attachment+ of the incoming email should be ignored + def accept_attachment?(attachment) + @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?) + @excluded.each do |pattern| + regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i + if attachment.filename.to_s =~ regexp + logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}" + return false + end + end + true + end + + # Adds To and Cc as watchers of the given object if the sender has the + # appropriate permission + def add_watchers(obj) + if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) + addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} + unless addresses.empty? + User.active.where('LOWER(mail) IN (?)', addresses).each do |w| + obj.add_watcher(w) + end + end + end + end + + def get_keyword(attr, options={}) + @keywords ||= {} + if @keywords.has_key?(attr) + @keywords[attr] + else + @keywords[attr] = begin + if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && + (v = extract_keyword!(plain_text_body, attr, options[:format])) + v + elsif !@@handler_options[:issue][attr].blank? + @@handler_options[:issue][attr] + end + end + end + end + + # Destructively extracts the value for +attr+ in +text+ + # Returns nil if no matching keyword found + def extract_keyword!(text, attr, format=nil) + keys = [attr.to_s.humanize] + if attr.is_a?(Symbol) + if user && user.language.present? + keys << l("field_#{attr}", :default => '', :locale => user.language) + end + if Setting.default_language.present? + keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) + end + end + keys.reject! {|k| k.blank?} + keys.collect! {|k| Regexp.escape(k)} + format ||= '.+' + keyword = nil + regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i + if m = text.match(regexp) + keyword = m[2].strip + text.gsub!(regexp, '') + end + keyword + end + + def target_project + # TODO: other ways to specify project: + # * parse the email To field + # * specific project (eg. Setting.mail_handler_target_project) + target = Project.find_by_identifier(get_keyword(:project)) + if target.nil? + # Invalid project keyword, use the project specified as the default one + default_project = @@handler_options[:issue][:project] + if default_project.present? + target = Project.find_by_identifier(default_project) + end + end + raise MissingInformation.new('Unable to determine target project') if target.nil? + target + end + + # Returns a Hash of issue attributes extracted from keywords in the email body + def issue_attributes_from_keywords(issue) + assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue) + + attrs = { + 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id), + 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id), + 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), + 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), + 'assigned_to_id' => assigned_to.try(:id), + 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && + issue.project.shared_versions.named(k).first.try(:id), + 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'estimated_hours' => get_keyword(:estimated_hours, :override => true), + 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') + }.delete_if {|k, v| v.blank? } + + if issue.new_record? && attrs['tracker_id'].nil? + attrs['tracker_id'] = issue.project.trackers.first.try(:id) + end + + attrs + end + + # Returns a Hash of issue custom field values extracted from keywords in the email body + def custom_field_values_from_keywords(customized) + customized.custom_field_values.inject({}) do |h, v| + if keyword = get_keyword(v.custom_field.name, :override => true) + h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) + end + h + end + end + + # Returns the text/plain part of the email + # If not found (eg. HTML-only email), returns the body with tags removed + def plain_text_body + return @plain_text_body unless @plain_text_body.nil? + + parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present? + text_parts + elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present? + html_parts + else + [email] + end + + parts.reject! do |part| + part.header[:content_disposition].try(:disposition_type) == 'attachment' + end + + @plain_text_body = parts.map do |p| + body_charset = p.charset.respond_to?(:force_encoding) ? + Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset + Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset) + end.join("\r\n") + + # strip html tags and remove doctype directive + if parts.any? {|p| p.mime_type == 'text/html'} + @plain_text_body = strip_tags(@plain_text_body.strip) + @plain_text_body.sub! %r{^$/) + addr, name = m[2], m[1] + end + if addr.present? + user = self.class.new_user_from_attributes(addr, name) + if @@handler_options[:no_notification] + user.mail_notification = 'none' + end + if user.save + user + else + logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger + nil + end + else + logger.error "MailHandler: failed to create User: no FROM address found" if logger + nil + end + end + + # Adds the newly created user to default group + def add_user_to_group(default_group) + if default_group.present? + default_group.split(',').each do |group_name| + if group = Group.named(group_name).first + group.users << @user + elsif logger + logger.warn "MailHandler: could not add user to [#{group_name}], group not found" + end + end + end + end + + # Removes the email body of text after the truncation configurations. + def cleanup_body(body) + delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} + unless delimiters.empty? + regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) + body = body.gsub(regex, '') + end + body.strip + end + + def find_assignee_from_keyword(keyword, issue) + keyword = keyword.to_s.downcase + assignable = issue.assignable_users + assignee = nil + assignee ||= assignable.detect {|a| + a.mail.to_s.downcase == keyword || + a.login.to_s.downcase == keyword + } + if assignee.nil? && keyword.match(/ /) + firstname, lastname = *(keyword.split) # "First Last Throwaway" + assignee ||= assignable.detect {|a| + a.is_a?(User) && a.firstname.to_s.downcase == firstname && + a.lastname.to_s.downcase == lastname + } + end + if assignee.nil? + assignee ||= assignable.detect {|a| a.name.downcase == keyword} + end + assignee + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/16931f56b13ff8b173c90b0717bce65066b04a78.svn-base --- a/.svn/pristine/16/16931f56b13ff8b173c90b0717bce65066b04a78.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,244 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'my_controller' - -# Re-raise errors caught by the controller. -class MyController; def rescue_action(e) raise e end; end - -class MyControllerTest < ActionController::TestCase - fixtures :users, :user_preferences, :roles, :projects, :members, :member_roles, - :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources - - def setup - @controller = MyController.new - @request = ActionController::TestRequest.new - @request.session[:user_id] = 2 - @response = ActionController::TestResponse.new - end - - def test_index - get :index - assert_response :success - assert_template 'page' - end - - def test_page - get :page - assert_response :success - assert_template 'page' - end - - def test_page_with_timelog_block - preferences = User.find(2).pref - preferences[:my_page_layout] = {'top' => ['timelog']} - preferences.save! - TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10) - - get :page - assert_response :success - assert_select 'tr.time-entry' do - assert_select 'td.subject a[href=/issues/1]' - assert_select 'td.hours', :text => '2.50' - end - end - - def test_my_account_should_show_editable_custom_fields - get :account - assert_response :success - assert_template 'account' - assert_equal User.find(2), assigns(:user) - - assert_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} - end - - def test_my_account_should_not_show_non_editable_custom_fields - UserCustomField.find(4).update_attribute :editable, false - - get :account - assert_response :success - assert_template 'account' - assert_equal User.find(2), assigns(:user) - - assert_no_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} - end - - def test_update_account - post :account, - :user => { - :firstname => "Joe", - :login => "root", - :admin => 1, - :group_ids => ['10'], - :custom_field_values => {"4" => "0100562500"} - } - - assert_redirected_to '/my/account' - user = User.find(2) - assert_equal user, assigns(:user) - assert_equal "Joe", user.firstname - assert_equal "jsmith", user.login - assert_equal "0100562500", user.custom_value_for(4).value - # ignored - assert !user.admin? - assert user.groups.empty? - end - - def test_my_account_should_show_destroy_link - get :account - assert_select 'a[href=/my/account/destroy]' - end - - def test_get_destroy_should_display_the_destroy_confirmation - get :destroy - assert_response :success - assert_template 'destroy' - assert_select 'form[action=/my/account/destroy]' do - assert_select 'input[name=confirm]' - end - end - - def test_post_destroy_without_confirmation_should_not_destroy_account - assert_no_difference 'User.count' do - post :destroy - end - assert_response :success - assert_template 'destroy' - end - - def test_post_destroy_without_confirmation_should_destroy_account - assert_difference 'User.count', -1 do - post :destroy, :confirm => '1' - end - assert_redirected_to '/' - assert_match /deleted/i, flash[:notice] - end - - def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account - User.any_instance.stubs(:own_account_deletable?).returns(false) - - assert_no_difference 'User.count' do - post :destroy, :confirm => '1' - end - assert_redirected_to '/my/account' - end - - def test_change_password - get :password - assert_response :success - assert_template 'password' - - # non matching password confirmation - post :password, :password => 'jsmith', - :new_password => 'secret123', - :new_password_confirmation => 'secret1234' - assert_response :success - assert_template 'password' - assert_error_tag :content => /Password doesn't match confirmation/ - - # wrong password - post :password, :password => 'wrongpassword', - :new_password => 'secret123', - :new_password_confirmation => 'secret123' - assert_response :success - assert_template 'password' - assert_equal 'Wrong password', flash[:error] - - # good password - post :password, :password => 'jsmith', - :new_password => 'secret123', - :new_password_confirmation => 'secret123' - assert_redirected_to '/my/account' - assert User.try_to_login('jsmith', 'secret123') - end - - def test_change_password_should_redirect_if_user_cannot_change_its_password - User.find(2).update_attribute(:auth_source_id, 1) - - get :password - assert_not_nil flash[:error] - assert_redirected_to '/my/account' - end - - def test_page_layout - get :page_layout - assert_response :success - assert_template 'page_layout' - end - - def test_add_block - post :add_block, :block => 'issuesreportedbyme' - assert_redirected_to '/my/page_layout' - assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme') - end - - def test_add_invalid_block_should_redirect - post :add_block, :block => 'invalid' - assert_redirected_to '/my/page_layout' - end - - def test_remove_block - post :remove_block, :block => 'issuesassignedtome' - assert_redirected_to '/my/page_layout' - assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome') - end - - def test_order_blocks - xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews'] - assert_response :success - assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left'] - end - - def test_reset_rss_key_with_existing_key - @previous_token_value = User.find(2).rss_key # Will generate one if it's missing - post :reset_rss_key - - assert_not_equal @previous_token_value, User.find(2).rss_key - assert User.find(2).rss_token - assert_match /reset/, flash[:notice] - assert_redirected_to '/my/account' - end - - def test_reset_rss_key_without_existing_key - assert_nil User.find(2).rss_token - post :reset_rss_key - - assert User.find(2).rss_token - assert_match /reset/, flash[:notice] - assert_redirected_to '/my/account' - end - - def test_reset_api_key_with_existing_key - @previous_token_value = User.find(2).api_key # Will generate one if it's missing - post :reset_api_key - - assert_not_equal @previous_token_value, User.find(2).api_key - assert User.find(2).api_token - assert_match /reset/, flash[:notice] - assert_redirected_to '/my/account' - end - - def test_reset_api_key_without_existing_key - assert_nil User.find(2).api_token - post :reset_api_key - - assert User.find(2).api_token - assert_match /reset/, flash[:notice] - assert_redirected_to '/my/account' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/16c33740f5a772b206a960577ab56df891c0f33f.svn-base --- a/.svn/pristine/16/16c33740f5a772b206a960577ab56df891c0f33f.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -CREATE TABLE 'companies' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'name' TEXT DEFAULT NULL, - 'rating' INTEGER DEFAULT 1 -); - -CREATE TABLE 'replies' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'content' text, - 'created_at' datetime, - 'updated_at' datetime, - 'topic_id' integer -); - -CREATE TABLE 'topics' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'title' varchar(255), - 'subtitle' varchar(255), - 'content' text, - 'created_at' datetime, - 'updated_at' datetime -); - -CREATE TABLE 'developers' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'name' TEXT DEFAULT NULL, - 'salary' INTEGER DEFAULT 70000, - 'created_at' DATETIME DEFAULT NULL, - 'updated_at' DATETIME DEFAULT NULL -); - -CREATE TABLE 'projects' ( - 'id' INTEGER PRIMARY KEY NOT NULL, - 'name' TEXT DEFAULT NULL -); - -CREATE TABLE 'developers_projects' ( - 'developer_id' INTEGER NOT NULL, - 'project_id' INTEGER NOT NULL, - 'joined_on' DATE DEFAULT NULL, - 'access_level' INTEGER DEFAULT 1 -); diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/16/16e91dd78763ecc12e60528188f6cb6512517aa6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/16/16e91dd78763ecc12e60528188f6cb6512517aa6.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,79 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RoutingTrackersTest < ActionController::IntegrationTest + def test_trackers + assert_routing( + { :method => 'get', :path => "/trackers" }, + { :controller => 'trackers', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/trackers.xml" }, + { :controller => 'trackers', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/trackers" }, + { :controller => 'trackers', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/trackers.xml" }, + { :controller => 'trackers', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/new" }, + { :controller => 'trackers', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/new.xml" }, + { :controller => 'trackers', :action => 'new', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/1/edit" }, + { :controller => 'trackers', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/trackers/1" }, + { :controller => 'trackers', :action => 'update', + :id => '1' } + ) + assert_routing( + { :method => 'put', :path => "/trackers/1.xml" }, + { :controller => 'trackers', :action => 'update', + :format => 'xml', :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/trackers/1" }, + { :controller => 'trackers', :action => 'destroy', + :id => '1' } + ) + assert_routing( + { :method => 'delete', :path => "/trackers/1.xml" }, + { :controller => 'trackers', :action => 'destroy', + :format => 'xml', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/trackers/fields" }, + { :controller => 'trackers', :action => 'fields' } + ) + assert_routing( + { :method => 'post', :path => "/trackers/fields" }, + { :controller => 'trackers', :action => 'fields' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/1751b405084d5f3027dcc49f2f10ccbdc9c84321.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/1751b405084d5f3027dcc49f2f10ccbdc9c84321.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,260 @@ +require 'active_record' + +module ActiveRecord + class Base + include Redmine::I18n + # Translate attribute names for validation errors display + def self.human_attribute_name(attr, *args) + attr = attr.to_s.sub(/_id$/, '') + + l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr]) + end + end + + # Undefines private Kernel#open method to allow using `open` scopes in models. + # See Defect #11545 (http://www.redmine.org/issues/11545) for details. + class Base + class << self + undef open + end + end + class Relation ; undef open ; end +end + +module ActionView + module Helpers + module DateHelper + # distance_of_time_in_words breaks when difference is greater than 30 years + def distance_of_date_in_words(from_date, to_date = 0, options = {}) + from_date = from_date.to_date if from_date.respond_to?(:to_date) + to_date = to_date.to_date if to_date.respond_to?(:to_date) + distance_in_days = (to_date - from_date).abs + + I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale| + case distance_in_days + when 0..60 then locale.t :x_days, :count => distance_in_days.round + when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round + else locale.t :over_x_years, :count => (distance_in_days / 365).floor + end + end + end + end + end + + class Resolver + def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) + cached(key, [name, prefix, partial], details, locals) do + if details[:formats] & [:xml, :json] + details = details.dup + details[:formats] = details[:formats].dup + [:api] + end + find_templates(name, prefix, partial, details) + end + end + end +end + +# Do not HTML escape text templates +module ActionView + class Template + module Handlers + class ERB + def call(template) + if template.source.encoding_aware? + # First, convert to BINARY, so in case the encoding is + # wrong, we can still find an encoding tag + # (<%# encoding %>) inside the String using a regular + # expression + template_source = template.source.dup.force_encoding("BINARY") + + erb = template_source.gsub(ENCODING_TAG, '') + encoding = $2 + + erb.force_encoding valid_encoding(template.source.dup, encoding) + + # Always make sure we return a String in the default_internal + erb.encode! + else + erb = template.source.dup + end + + self.class.erb_implementation.new( + erb, + :trim => (self.class.erb_trim_mode == "-"), + :escape => template.identifier =~ /\.text/ # only escape HTML templates + ).src + end + end + end + end +end + +ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe } + +# HTML5: is invalid, use instead +module ActionView + module Helpers + class InstanceTag + private + def add_options_with_non_empty_blank_option(option_tags, options, value = nil) + if options[:include_blank] == true + options = options.dup + options[:include_blank] = ' '.html_safe + end + add_options_without_non_empty_blank_option(option_tags, options, value) + end + alias_method_chain :add_options, :non_empty_blank_option + end + + module FormTagHelper + def select_tag_with_non_empty_blank_option(name, option_tags = nil, options = {}) + if options.delete(:include_blank) + options[:prompt] = ' '.html_safe + end + select_tag_without_non_empty_blank_option(name, option_tags, options) + end + alias_method_chain :select_tag, :non_empty_blank_option + end + + module FormOptionsHelper + def options_for_select_with_non_empty_blank_option(container, selected = nil) + if container.is_a?(Array) + container = container.map {|element| element.blank? ? [" ".html_safe, ""] : element} + end + options_for_select_without_non_empty_blank_option(container, selected) + end + alias_method_chain :options_for_select, :non_empty_blank_option + end + end +end + +require 'mail' + +module DeliveryMethods + class AsyncSMTP < ::Mail::SMTP + def deliver!(*args) + Thread.start do + super *args + end + end + end + + class AsyncSendmail < ::Mail::Sendmail + def deliver!(*args) + Thread.start do + super *args + end + end + end + + class TmpFile + def initialize(*args); end + + def deliver!(mail) + dest_dir = File.join(Rails.root, 'tmp', 'emails') + Dir.mkdir(dest_dir) unless File.directory?(dest_dir) + File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) } + end + end +end + +ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP +ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail +ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile + +# Changes how sent emails are logged +# Rails doesn't log cc and bcc which is misleading when using bcc only (#12090) +module ActionMailer + class LogSubscriber < ActiveSupport::LogSubscriber + def deliver(event) + recipients = [:to, :cc, :bcc].inject("") do |s, header| + r = Array.wrap(event.payload[header]) + if r.any? + s << "\n #{header}: #{r.join(', ')}" + end + s + end + info("\nSent email \"#{event.payload[:subject]}\" (%1.fms)#{recipients}" % event.duration) + debug(event.payload[:mail]) + end + end +end + +module ActionController + module MimeResponds + class Collector + def api(&block) + any(:xml, :json, &block) + end + end + end +end + +module ActionController + class Base + # Displays an explicit message instead of a NoMethodError exception + # when trying to start Redmine with an old session_store.rb + # TODO: remove it in a later version + def self.session=(*args) + $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" + + "Setting the session secret with ActionController.session= is no longer supported in Rails 3." + exit 1 + end + end +end + +if Rails::VERSION::MAJOR < 4 && RUBY_VERSION >= "2.1" + module ActiveSupport + class HashWithIndifferentAccess + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + end + + class OrderedHash + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + end + end +end + +require 'awesome_nested_set/version' + +module CollectiveIdea + module Acts + module NestedSet + module Model + def leaf_with_new_record? + new_record? || leaf_without_new_record? + end + alias_method_chain :leaf?, :new_record + # Reload is needed because children may have updated + # their parent (self) during deletion. + if ::AwesomeNestedSet::VERSION > "2.1.6" + module Prunable + def destroy_descendants_with_reload + destroy_descendants_without_reload + reload + end + alias_method_chain :destroy_descendants, :reload + end + else + def destroy_descendants_with_reload + destroy_descendants_without_reload + reload + end + alias_method_chain :destroy_descendants, :reload + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/1760e8859f909defc3298edd73c13960cad4c773.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/1760e8859f909defc3298edd73c13960cad4c773.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,181 @@ +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 + + # Generates an unsaved Issue + 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 + end + + # Generates a saved Issue + def Issue.generate!(attributes={}, &block) + issue = Issue.generate(attributes, &block) + 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 + + def Changeset.generate!(attributes={}) + @generated_changeset_rev ||= '123456' + @generated_changeset_rev.succ! + changeset = new(attributes) + changeset.repository ||= Project.find(1).repository + changeset.revision ||= @generated_changeset_rev + changeset.committed_on ||= Time.now + yield changeset if block_given? + changeset.save! + changeset + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/176ab48d54517bf4b7c43dc6852d904f4bfffc5a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/176ab48d54517bf4b7c43dc6852d904f4bfffc5a.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,54 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 d98d22a98252 -r fb9a13467253 .svn/pristine/17/176d1dc144c5bd598057e3a7c94346dee9aedc08.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/176d1dc144c5bd598057e3a7c94346dee9aedc08.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,13 @@ +class AddViewIssuesPermission < ActiveRecord::Migration + def self.up + Role.all.each do |r| + r.add_permission!(:view_issues) + end + end + + def self.down + Role.all.each do |r| + r.remove_permission!(:view_issues) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/178baed3b8d7cd58a7e5bbe97a67c4757201c153.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/178baed3b8d7cd58a7e5bbe97a67c4757201c153.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,53 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 PreviewsController < ApplicationController + before_filter :find_project, :find_attachments + + def issue + @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? + if @issue + @description = params[:issue] && params[:issue][:description] + if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n") + @description = nil + end + # params[:notes] is useful for preview of notes in issue history + @notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil) + else + @description = (params[:issue] ? params[:issue][:description] : nil) + end + render :layout => false + end + + def news + if params[:id].present? && news = News.visible.find_by_id(params[:id]) + @previewed = news + end + @text = (params[:news] ? params[:news][:description] : nil) + render :partial => 'common/preview' + end + + private + + def find_project + project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/17973741c668d43d267b734fcb018ab64a0d87f9.svn-base --- a/.svn/pristine/17/17973741c668d43d267b734fcb018ab64a0d87f9.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -

    <%= l(:label_plugins) %>

    - -<% if @plugins.any? %> - - <% @plugins.each do |plugin| %> - - - - - - - <% end %> -
    <%=h plugin.name %> - <%= content_tag('span', h(plugin.description), :class => 'description') unless plugin.description.blank? %> - <%= content_tag('span', link_to(h(plugin.url), plugin.url), :class => 'url') unless plugin.url.blank? %> - <%= plugin.author_url.blank? ? h(plugin.author) : link_to(h(plugin.author), plugin.author_url) %><%=h plugin.version %><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.id) if plugin.configurable? %>
    -<% else %> -

    <%= l(:label_no_data) %>

    -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/17bbb18f9332f2fb1c968be77b1ba5e516d1b601.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/17bbb18f9332f2fb1c968be77b1ba5e516d1b601.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,45 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::CustomFieldsTest < Redmine::ApiTest::Base + fixtures :users, :custom_fields + + def setup + Setting.rest_api_enabled = '1' + end + + test "GET /custom_fields.xml should return custom fields" do + get '/custom_fields.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'custom_fields' do + assert_select 'custom_field' do + assert_select 'name', :text => 'Database' + assert_select 'id', :text => '2' + assert_select 'customized_type', :text => 'issue' + assert_select 'possible_values[type=array]' do + assert_select 'possible_value>value', :text => 'PostgreSQL' + end + assert_select 'trackers[type=array]' + assert_select 'roles[type=array]' + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/17c033d2aba8bd48a0c5240eb040a0c6c95834ea.svn-base --- a/.svn/pristine/17/17c033d2aba8bd48a0c5240eb040a0c6c95834ea.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -class IssueMove < ActiveRecord::Migration - # model removed - class Permission < ActiveRecord::Base; end - - def self.up - Permission.create :controller => "projects", :action => "move_issues", :description => "button_move", :sort => 1061, :mail_option => 0, :mail_enabled => 0 - end - - def self.down - Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'move_issues']).destroy - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/17c53021398a51ccae878d86cdd4f0a66880e996.svn-base --- a/.svn/pristine/17/17c53021398a51ccae878d86cdd4f0a66880e996.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -class AddCommentsPermissions < ActiveRecord::Migration - # model removed - class Permission < ActiveRecord::Base; end - - def self.up - Permission.create :controller => "news", :action => "add_comment", :description => "label_comment_add", :sort => 1130, :is_public => false, :mail_option => 0, :mail_enabled => 0 - Permission.create :controller => "news", :action => "destroy_comment", :description => "label_comment_delete", :sort => 1133, :is_public => false, :mail_option => 0, :mail_enabled => 0 - end - - def self.down - Permission.find(:first, :conditions => ["controller=? and action=?", 'news', 'add_comment']).destroy - Permission.find(:first, :conditions => ["controller=? and action=?", 'news', 'destroy_comment']).destroy - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/17/17cae769d03ec609655de7a5f618f9f07f958119.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/17/17cae769d03ec609655de7a5f618f9f07f958119.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,72 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 MemberRole < ActiveRecord::Base + belongs_to :member + belongs_to :role + + after_destroy :remove_member_if_empty + + after_create :add_role_to_group_users, :add_role_to_subprojects + after_destroy :remove_inherited_roles + + validates_presence_of :role + validate :validate_role_member + + def validate_role_member + errors.add :role_id, :invalid if role && !role.member? + end + + def inherited? + !inherited_from.nil? + end + + private + + def remove_member_if_empty + if member.roles.empty? + member.destroy + end + end + + def add_role_to_group_users + if member.principal.is_a?(Group) && !inherited? + member.principal.users.each do |user| + user_member = Member.find_or_new(member.project_id, user.id) + user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id) + user_member.save! + end + end + end + + def add_role_to_subprojects + member.project.children.each do |subproject| + if subproject.inherit_members? + child_member = Member.find_or_new(subproject.id, member.user_id) + child_member.member_roles << MemberRole.new(:role => role, :inherited_from => id) + child_member.save! + end + end + end + + def remove_inherited_roles + MemberRole.where(:inherited_from => id).group_by(&:member). + each do |member, member_roles| + member_roles.each(&:destroy) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/1834715ab6ce08d2e0bc1e49b4f06e012364d78b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/1834715ab6ce08d2e0bc1e49b4f06e012364d78b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,21 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 AccountHelper +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/18371ec7cebc4aa9d96854358cc5d6472c968d96.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/18371ec7cebc4aa9d96854358cc5d6472c968d96.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1112 @@ +# Bulgarian translation by Nikolay Solakov and Ivan Cenov +bg: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + 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: "%d-%m-%Y" + 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: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "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: + format: + separator: "." + delimiter: "" + precision: 3 + + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: байт + other: байта + 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: "1 грешка попречи този %{model} да бъде запиÑан" + other: "%{count} грешки попречиха този %{model} да бъде запиÑан" + messages: + inclusion: "не ÑъщеÑтвува в ÑпиÑъка" + exclusion: "е запазено" + invalid: "е невалидно" + confirmation: "липÑва одобрение" + accepted: "трÑбва да Ñе приеме" + empty: "не може да е празно" + blank: "не може да е празно" + too_long: "е прекалено дълго" + too_short: "е прекалено къÑо" + wrong_length: "е Ñ Ð³Ñ€ÐµÑˆÐ½Ð° дължина" + taken: "вече ÑъщеÑтвува" + not_a_number: "не е чиÑло" + not_a_date: "е невалидна дата" + greater_than: "трÑбва да бъде по-голÑм[a/о] от %{count}" + greater_than_or_equal_to: "трÑбва да бъде по-голÑм[a/о] от или равен[a/o] на %{count}" + equal_to: "трÑбва да бъде равен[a/o] на %{count}" + less_than: "трÑбва да бъде по-малък[a/o] от %{count}" + less_than_or_equal_to: "трÑбва да бъде по-малък[a/o] от или равен[a/o] на %{count}" + odd: "трÑбва да бъде нечетен[a/o]" + even: "трÑбва да бъде четен[a/o]" + greater_than_start_date: "трÑбва да е Ñлед началната дата" + not_same_project: "не е от ÑÑŠÑ‰Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚" + circular_dependency: "Тази Ñ€ÐµÐ»Ð°Ñ†Ð¸Ñ Ñ‰Ðµ доведе до безкрайна завиÑимоÑÑ‚" + cant_link_an_issue_with_a_descendant: "Една задача не може да бъде Ñвързвана към ÑÐ²Ð¾Ñ Ð¿Ð¾Ð´Ð·Ð°Ð´Ð°Ñ‡Ð°" + earlier_than_minimum_start_date: "не може да бъде по-рано от %{date} поради предхождащи задачи" + + actionview_instancetag_blank_option: Изберете + + general_text_No: 'Ðе' + general_text_Yes: 'Да' + general_text_no: 'не' + general_text_yes: 'да' + general_lang_name: 'Bulgarian (БългарÑки)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Профилът е обновен уÑпешно. + notice_account_invalid_creditentials: Ðевалиден потребител или парола. + notice_account_password_updated: Паролата е уÑпешно променена. + notice_account_wrong_password: Грешна парола + notice_account_register_done: Профилът е Ñъздаден уÑпешно. E-mail, Ñъдържащ инÑтрукции за активиране на профила + е изпратен на %{email}. + notice_account_unknown_email: Ðепознат e-mail. + notice_account_not_activated_yet: Вие не Ñте активирали Ð²Ð°ÑˆÐ¸Ñ Ð¿Ñ€Ð¾Ñ„Ð¸Ð» вÑе още. Ðко иÑкате да + получите нов e-mail за активиране, Ð¼Ð¾Ð»Ñ Ð½Ð°Ñ‚Ð¸Ñнете тази връзка. + notice_account_locked: ВашиÑÑ‚ профил е блокиран. + notice_can_t_change_password: Този профил е Ñ Ð²ÑŠÐ½ÑˆÐµÐ½ метод за оторизациÑ. Ðевъзможна ÑмÑна на паролата. + notice_account_lost_email_sent: Изпратен ви е e-mail Ñ Ð¸Ð½Ñтрукции за избор на нова парола. + notice_account_activated: Профилът ви е активиран. Вече може да влезете в Redmine. + 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: "Изпратен e-mail на %{value}" + notice_email_error: "Грешка при изпращане на e-mail (%{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_time_entries: "ÐевъзможноÑÑ‚ за Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° %{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}) + notice_issue_successful_create: Задача %{id} е Ñъздадена. + notice_issue_update_conflict: Задачата е била променена от друг потребител, докато вие Ñте Ñ Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð°Ð»Ð¸. + notice_account_deleted: ВашиÑÑ‚ профил беше премахнат без възможноÑÑ‚ за възÑтановÑване. + notice_user_successful_create: Потребител %{id} е Ñъздаден. + notice_new_password_must_be_different: Ðовата парола трÑбва да бъде различна от Ñегашната парола + + error_can_t_load_default_data: "Грешка при зареждане на началната информациÑ: %{value}" + error_scm_not_found: ÐеÑъщеÑтвуващ обект в хранилището. + error_scm_command_failed: "Грешка при опит за ÐºÐ¾Ð¼ÑƒÐ½Ð¸ÐºÐ°Ñ†Ð¸Ñ Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð»Ð¸Ñ‰Ðµ: %{value}" + error_scm_annotate: "Обектът не ÑъщеÑтвува или не може да бъде анотиран." + error_scm_annotate_big_text_file: "Файлът не може да бъде анотиран, понеже Ð½Ð°Ð´Ñ…Ð²ÑŠÑ€Ð»Ñ Ð¼Ð°ÐºÑÐ¸Ð¼Ð°Ð»Ð½Ð¸Ñ Ñ€Ð°Ð·Ð¼ÐµÑ€ за текÑтови файлове." + 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: ÐœÐ¾Ð»Ñ Ð¸Ð·Ð±ÐµÑ€ÐµÑ‚Ðµ source тракер или Ñ€Ð¾Ð»Ñ + error_workflow_copy_target: ÐœÐ¾Ð»Ñ Ð¸Ð·Ð±ÐµÑ€ÐµÑ‚Ðµ тракер(и) и Ñ€Ð¾Ð»Ñ (роли). + error_unable_delete_issue_status: ÐевъзможноÑÑ‚ за изтриване на ÑÑŠÑтоÑние на задача + error_unable_to_connect: ÐевъзможноÑÑ‚ за Ñвързване Ñ (%{value}) + error_attachment_too_big: Този файл не може да бъде качен, понеже Ð½Ð°Ð´Ñ…Ð²ÑŠÑ€Ð»Ñ Ð¼Ð°ÐºÑималната възможна големина (%{max_size}) + error_session_expired: Вашата ÑеÑÐ¸Ñ Ðµ изтекла. ÐœÐ¾Ð»Ñ Ð²Ð»ÐµÐ·ÐµÑ‚Ðµ в Redmine отново. + 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: "Wiki Ñтраницата '%{id}' беше добавена" + mail_body_wiki_content_added: Wiki Ñтраницата '%{id}' беше добавена от %{author}. + mail_subject_wiki_content_updated: "Wiki Ñтраницата '%{id}' беше обновена" + mail_body_wiki_content_updated: Wiki Ñтраницата '%{id}' беше обновена от %{author}. + + field_name: Име + field_description: ОпиÑание + field_summary: ÐÐ½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ + field_is_required: Задължително + field_firstname: Име + field_lastname: Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ + field_mail: Email + field_filename: Файл + field_filesize: Големина + field_downloads: Изтеглени файлове + field_author: Ðвтор + field_created_on: От дата + field_updated_on: Обновена + field_closed_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: 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: Ðтрибут Login + field_attr_firstname: Ðтрибут Първо име (Firstname) + field_attr_lastname: Ðтрибут Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ (Lastname) + field_attr_mail: Ðтрибут Email + field_onthefly: Динамично Ñъздаване на потребител + field_start_date: Ðачална дата + field_done_ratio: "% ПрогреÑ" + field_auth_source: Ðачин на Ð¾Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ + field_hide_mail: Скрий e-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: Log time + field_time_zone: ЧаÑова зона + field_searchable: С възможноÑÑ‚ за търÑене + field_default_value: СтойноÑÑ‚ по подразбиране + field_comments_sorting: Сортиране на коментарите + field_parent_title: РодителÑка Ñтраница + field_editable: Editable + field_watcher: Ðаблюдател + field_identity_url: OpenID URL + field_content: Съдържание + field_group_by: Групиране на резултатите по + field_sharing: Sharing + field_parent_issue: РодителÑка задача + field_member_of_group: Член на група + field_assigned_to_role: Assignee's role + field_text: ТекÑтово поле + field_visible: Видим + field_warn_on_leaving_unsaved: Предупреди ме, когато напуÑкам Ñтраница Ñ Ð½ÐµÐ·Ð°Ð¿Ð¸Ñано Ñъдържание + field_issues_visibility: ВидимоÑÑ‚ на задачите + field_is_private: Лична + field_commit_logs_encoding: Кодова таблица на ÑъобщениÑта при поверÑване + field_scm_path_encoding: Кодова таблица на пътищата (path) + field_path_to_repository: Път до хранилището + field_root_directory: Коренна Ð´Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ (папка) + field_cvsroot: CVSROOT + field_cvs_module: Модул + field_repository_is_default: Главно хранилище + field_multiple: Избор на повече от една ÑтойноÑÑ‚ + field_auth_source_ldap_filter: LDAP филтър + field_core_fields: Стандартни полета + field_timeout: Таймаут (в Ñекунди) + field_board_parent: РодителÑки форум + field_private_notes: Лични бележки + field_inherit_members: ÐаÑледÑване на членовете на родителÑÐºÐ¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚ + field_generate_password: Генериране на парола + field_must_change_passwd: Паролата трÑбва да бъде Ñменена при Ñледващото влизане в Redmine + + setting_app_title: Заглавие + setting_app_subtitle: ОпиÑание + setting_welcome_text: Допълнителен текÑÑ‚ + setting_default_language: Език по подразбиране + setting_login_required: ИзиÑкване за вход в Redmine + setting_self_registration: РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¾Ñ‚ потребители + setting_attachment_max_size: МакÑимална големина на прикачен файл + setting_issues_export_limit: МакÑимален брой задачи за екÑпорт + setting_mail_from: E-mail Ð°Ð´Ñ€ÐµÑ Ð·Ð° емиÑии + setting_bcc_recipients: Получатели на Ñкрито копие (bcc) + setting_plain_text_mail: Ñамо чиÑÑ‚ текÑÑ‚ (без HTML) + setting_host_name: ХоÑÑ‚ + setting_text_formatting: Форматиране на текÑта + setting_wiki_compression: КомпреÑиране на Wiki иÑториÑта + setting_feeds_limit: МакÑимален брой запиÑи в ATOM емиÑии + setting_default_projects_public: Ðовите проекти Ñа публични по подразбиране + setting_autofetch_changesets: Ðвтоматично извличане на ревизиите + setting_sys_api_enabled: Разрешаване на WS за управление + setting_commit_ref_keywords: ОтбелÑзващи ключови думи + setting_commit_fix_keywords: Приключващи ключови думи + setting_autologin: Ðвтоматичен вход + setting_date_format: Формат на датата + setting_time_format: Формат на чаÑа + setting_cross_project_issue_relations: Релации на задачи между проекти + setting_cross_project_subtasks: Подзадачи от други проекти + setting_issue_list_default_columns: Показвани колони по подразбиране + setting_repositories_encodings: Кодова таблица на прикачените файлове и хранилищата + setting_emails_header: Email header + setting_emails_footer: ПодтекÑÑ‚ за e-mail + 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: ОтрÑзване на e-mail-ите Ñлед един от тези редове + setting_mail_handler_api_enabled: Разрешаване на WS за входÑщи e-mail-и + setting_mail_handler_api_key: API ключ + setting_sequential_project_identifiers: Генериране на поÑледователни проектни идентификатори + setting_gravatar_enabled: Използване на портребителÑки икони от Gravatar + setting_gravatar_default: Подразбиращо Ñе изображение от Gravatar + setting_diff_max_lines_displayed: МакÑимален брой показвани diff редове + setting_file_max_size_displayed: МакÑимален размер на текÑтовите файлове, показвани inline + 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 ÑÑŠÑ€Ð²Ð¸Ñ + setting_cache_formatted_text: Кеширане на форматираните текÑтове + setting_default_notification_option: Подразбиращ Ñе начин за извеÑÑ‚Ñване + setting_commit_logtime_enabled: Разрешаване на отчитането на работното време + setting_commit_logtime_activity_id: ДейноÑÑ‚ при отчитане на работното време + setting_gantt_items_limit: МакÑимален брой обекти, които да Ñе показват в мрежов график + setting_issue_group_assignment: Разрешено назначаването на задачи на групи + setting_default_issue_start_date_to_creation_date: Ðачална дата на новите задачи по подразбиране да бъде днешната дата + setting_commit_cross_project_ref: ОтбелÑзване и приключване на задачи от други проекти, неÑвързани Ñ ÐºÐ¾Ð½ÐºÑ€ÐµÑ‚Ð½Ð¾Ñ‚Ð¾ хранилище + setting_unsubscribe: Потребителите могат да премахват профилите Ñи + setting_session_lifetime: МакÑимален живот на ÑеÑиите + setting_session_timeout: Таймаут за неактивноÑÑ‚ преди прекратÑване на ÑеÑиите + setting_thumbnails_enabled: Показване на миниатюри на прикачените Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ + setting_thumbnails_size: Размер на миниатюрите (в пикÑели) + setting_non_working_week_days: Ðе работни дни + setting_jsonp_enabled: Разрешаване на поддръжка на JSONP + setting_default_projects_tracker_ids: Тракери по подразбиране за нови проекти + setting_mail_handler_excluded_filenames: Имена на прикачени файлове, които да Ñе пропуÑкат при приемане на e-mail-и (например *.vcf, companylogo.gif). + setting_force_default_language_for_anonymous: Задължително език по подразбиране за анонимните потребители + setting_force_default_language_for_loggedin: Задължително език по подразбиране за потребителите, влезли в Redmine + + permission_add_project: Създаване на проект + permission_add_subprojects: Създаване на подпроекти + permission_edit_project: Редактиране на проект + permission_close_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_set_own_issues_private: УÑтановÑване на ÑобÑтвените задачи публични или лични + permission_set_issues_private: УÑтановÑване на задачите публични или лични + permission_add_issue_notes: ДобавÑне на бележки + permission_edit_issue_notes: Редактиране на бележки + permission_edit_own_issue_notes: Редактиране на ÑобÑтвени бележки + permission_view_private_notes: Разглеждане на лични бележки + permission_set_notes_private: УÑтановÑване на бележките лични + permission_move_issues: ПремеÑтване на задачи + permission_delete_issues: Изтриване на задачи + permission_manage_public_queries: Управление на публичните заÑвки + permission_save_queries: Ð—Ð°Ð¿Ð¸Ñ Ð½Ð° Ð·Ð°Ð¿Ð¸Ñ‚Ð²Ð°Ð½Ð¸Ñ (queries) + permission_view_gantt: Разглеждане на мрежов график + permission_view_calendar: Разглеждане на календари + permission_view_issue_watchers: Разглеждане на ÑпиÑък Ñ Ð½Ð°Ð±Ð»ÑŽÐ´Ð°Ñ‚ÐµÐ»Ð¸ + permission_add_issue_watchers: ДобавÑне на наблюдатели + permission_delete_issue_watchers: Изтриване на наблюдатели + permission_log_time: Log spent time + permission_view_time_entries: Разглеждане на изразходваното време + permission_edit_time_entries: Редактиране на time logs + permission_edit_own_time_entries: Редактиране на ÑобÑтвените time logs + permission_manage_news: Управление на новини + permission_comment_news: Коментиране на новини + permission_view_documents: Разглеждане на документи + permission_add_documents: ДобавÑне на документи + permission_edit_documents: Редактиране на документи + permission_delete_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: Изтриване на прикачени файлове към wiki Ñтраници + permission_protect_wiki_pages: Заключване на wiki Ñтраници + permission_manage_repository: Управление на хранилища + permission_browse_repository: Разглеждане на хранилища + permission_view_changesets: Разглеждане на changesets + permission_commit_access: ПоверÑване + permission_manage_boards: Управление на 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: Управление на подзадачите + permission_manage_related_issues: Управление на връзките между задачи и ревизии + + 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: 0 проекта + 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_issue_note_added: Добавена бележка + label_issue_status_updated: Обновено ÑÑŠÑтоÑние + label_issue_priority_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_role_anonymous: Ðнонимен + label_role_non_member: Ðе член + 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_news_comment_added: Добавен коментар към новина + label_settings: ÐаÑтройки + label_overview: Общ изглед + label_version: ВерÑÐ¸Ñ + label_version_new: Ðова верÑÐ¸Ñ + label_version_plural: ВерÑии + label_close_versions: ЗатварÑне на завършените верÑии + label_confirmation: Одобрение + label_export_to: ЕкÑпорт към + label_read: 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_x_issues: + zero: 0 задачи + one: 1 задача + other: "%{count} задачи" + label_total: Общо + label_total_time: Общо + label_permissions: Права + label_current_status: Текущо ÑÑŠÑтоÑние + label_new_statuses_allowed: Позволени ÑÑŠÑтоÑÐ½Ð¸Ñ + label_all: вÑички + label_any: без значение + 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: 0 коментара + one: 1 коментар + other: "%{count} коментара" + label_comment_add: ДобавÑне на коментар + label_comment_added: Добавен коментар + label_comment_delete: Изтриване на коментари + label_query: ПотребителÑка Ñправка + label_query_plural: ПотребителÑки Ñправки + label_query_new: Ðова заÑвка + label_my_queries: Моите заÑвки + label_filter_add: Добави филтър + label_filter_plural: Филтри + label_equals: е + label_not_equals: не е + label_in_less_than: Ñлед по-малко от + label_in_more_than: Ñлед повече от + label_in_the_next_days: в Ñледващите + label_in_the_past_days: в предишните + label_greater_or_equal: ">=" + label_less_or_equal: <= + label_between: между + label_in: в Ñледващите + label_today: Ð´Ð½ÐµÑ + label_all_time: вÑички + label_yesterday: вчера + label_this_week: тази Ñедмица + label_last_week: поÑледната Ñедмица + label_last_n_weeks: поÑледните %{count} Ñедмици + 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_any_issues_in_project: задачи от проект + label_any_issues_not_in_project: задачи, които не Ñа в проект + label_no_issues_in_project: никакви задачи в проект + label_day_plural: дни + label_repository: Хранилище + label_repository_new: Ðово хранилище + 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: Pезултати + 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: ЕмиÑии + 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_diff: diff + 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_copied_to: копирана в + label_copied_from: копирана от + 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: 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: Изпращане на теÑтов e-mail + label_feeds_access_key: Atom access ключ + label_missing_feeds_access_key: ЛипÑващ Atom ключ за доÑтъп + label_feeds_access_key_created_on: "%{value} от Ñъздаването на Atom ключа" + 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_bulk_edit_selected_time_entries: Групово редактиране на запиÑи за използвано време + 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: активиране на профила по 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: ВходÑщи e-mail-и + 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: 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: Изпращане на Send e-mail извеÑÑ‚Ð¸Ñ Ð¿Ð¾ време на копирането на проекта + label_principal_search: "ТърÑене на потребител или група:" + label_user_search: "ТърÑене на потребител:" + label_additional_workflow_transitions_for_author: Позволени Ñа допълнителни преходи, когато потребителÑÑ‚ е авторът + label_additional_workflow_transitions_for_assignee: Позволени Ñа допълнителни преходи, когато потребителÑÑ‚ е назначениÑÑ‚ към задачата + label_issues_visibility_all: Ð’Ñички задачи + label_issues_visibility_public: Ð’Ñички не-лични задачи + label_issues_visibility_own: Задачи, Ñъздадени от или назначени на Ð¿Ð¾Ñ‚Ñ€ÐµÐ±Ð¸Ñ‚ÐµÐ»Ñ + label_git_report_last_commit: Извеждане на поÑледното поверÑване за файлове и папки + label_parent_revision: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ» + label_child_revision: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ Ð½Ð°Ñледник + label_export_options: "%{export_format} опции за екÑпорт" + label_copy_attachments: Копиране на прикачените файлове + label_copy_subtasks: Копиране на подзадачите + label_item_position: "%{position}/%{count}" + label_completed_versions: Завършени верÑии + label_search_for_watchers: ТърÑене на потребители за наблюдатели + label_session_expiration: Изтичане на ÑеÑиите + label_show_closed_projects: Разглеждане на затворени проекти + label_status_transitions: Преходи между ÑÑŠÑтоÑниÑта + label_fields_permissions: ВидимоÑÑ‚ на полетата + label_readonly: Само за четене + label_required: Задължително + label_hidden: Скрит + label_attribute_of_project: Project's %{name} + label_attribute_of_issue: Issue's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_user: User's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_cross_project_descendants: С подпроекти + label_cross_project_tree: С дърво на проектите + label_cross_project_hierarchy: С проектна Ð¹ÐµÑ€Ð°Ñ€Ñ…Ð¸Ñ + label_cross_project_system: С вÑички проекти + label_gantt_progress_line: Ð›Ð¸Ð½Ð¸Ñ Ð½Ð° изпълнението + label_visibility_private: лични (Ñамо за мен) + label_visibility_roles: Ñамо за тези роли + label_visibility_public: за вÑички потребители + label_link: Връзка + label_only: Ñамо + label_drop_down_list: drop-down ÑпиÑък + label_checkboxes: чек-Ð±Ð¾ÐºÑ + label_radio_buttons: радио-бутони + label_link_values_to: URL (опциÑ) + label_custom_field_select_type: "Изберете тип на обект, към който потребителÑкото поле да бъде аÑоциирано" + label_check_for_updates: Проверка за нови верÑии + label_latest_compatible_version: ПоÑледна ÑъвмеÑтима верÑÐ¸Ñ + label_unknown_plugin: Ðепознат плъгин + + button_login: Вход + button_submit: Изпращане + button_save: Ð—Ð°Ð¿Ð¸Ñ + button_check_all: Избор на вÑички + button_uncheck_all: ИзчиÑтване на вÑички + button_collapse_all: Скриване вÑички + button_expand_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: Показване + button_hide: Скриване + button_edit_section: Редактиране на тази ÑÐµÐºÑ†Ð¸Ñ + button_export: ЕкÑпорт + button_delete_my_account: Премахване на Ð¼Ð¾Ñ Ð¿Ñ€Ð¾Ñ„Ð¸Ð» + button_close: ЗатварÑне + button_reopen: ОтварÑне + + status_active: активен + status_registered: региÑтриран + status_locked: заключен + + project_status_active: активен + project_status_closed: затворен + project_status_archived: архивиран + + version_status_open: отворена + version_status_locked: заключена + version_status_closed: затворена + + field_active: Ðктивен + + text_select_mail_notifications: Изберете ÑÑŠÐ±Ð¸Ñ‚Ð¸Ñ Ð·Ð° изпращане на e-mail. + 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_changed_no_detail: "%{label} променен" + 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_project_identifier_info: 'Позволени Ñа малки букви (a-z), цифри, тирета и _.
    ПромÑна Ñлед Ñъздаването му не е възможна.' + 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_issues_destroy_descendants_confirmation: Тази Ð¾Ð¿ÐµÑ€Ð°Ñ†Ð¸Ñ Ñ‰Ðµ премахне и %{count} подзадача(и). + text_time_entries_destroy_confirmation: Сигурен ли Ñте, че изтриете избраните запиÑи за изразходвано време? + text_select_project_modules: 'Изберете активните модули за този проект:' + text_default_administrator_account_changed: Сменен Ñ„Ð°Ð±Ñ€Ð¸Ñ‡Ð½Ð¸Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸ÑтраторÑки профил + text_file_repository_writable: ВъзможноÑÑ‚ за пиÑане в хранилището Ñ Ñ„Ð°Ð¹Ð»Ð¾Ð²Ðµ + text_plugin_assets_writable: Папката на приÑтавките е разрешена за Ð·Ð°Ð¿Ð¸Ñ + text_rmagick_available: Ðаличен RMagick (по избор) + text_convert_available: Ðаличен ImageMagick convert (по избор) + 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: "Изпращането на e-mail-и не е конфигурирано и извеÑтиÑта не Ñа разрешени.\nКонфигурирайте Ð²Ð°ÑˆÐ¸Ñ SMTP Ñървър в config/configuration.yml и реÑтартирайте Redmine, за да ги разрешите." + text_repository_usernames_mapping: "Изберете или променете потребителите в Redmine, ÑъответÑтващи на потребителите в дневника на хранилището (repository).\nПотребителите Ñ ÐµÐ´Ð½Ð°ÐºÐ²Ð¸ имена в Redmine и хранилищата Ñе ÑъвмеÑÑ‚Ñват автоматично." + text_diff_truncated: '... Този diff не е пълен, понеже е Ð½Ð°Ð´Ñ…Ð²ÑŠÑ€Ð»Ñ Ð¼Ð°ÐºÑÐ¸Ð¼Ð°Ð»Ð½Ð¸Ñ Ñ€Ð°Ð·Ð¼ÐµÑ€, който може да бъде показан.' + text_custom_field_possible_values_info: 'Една ÑтойноÑÑ‚ на ред' + text_wiki_page_destroy_question: Тази Ñтраница има %{descendants} Ñтраници деца и descendant(s). Какво желаете да правите? + text_wiki_page_nullify_children: Запазване на тези Ñтраници като коренни Ñтраници + text_wiki_page_destroy_children: Изтриване на Ñтраниците деца и вÑички техни descendants + text_wiki_page_reassign_children: Преназначаване на Ñтраниците деца на тази родителÑка Ñтраница + text_own_membership_delete_confirmation: "Вие Ñте на път да премахнете нÑкои или вÑички ваши Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð¸ е възможно Ñлед това да не можете да редактирате този проект.\nСигурен ли Ñте, че иÑкате да продължите?" + text_zoom_in: Увеличаване + text_zoom_out: ÐамалÑване + text_warn_on_leaving_unsaved: Страницата Ñъдържа незапиÑано Ñъдържание, което може да бъде загубено, ако Ñ Ð½Ð°Ð¿ÑƒÑнете. + text_scm_path_encoding_note: "По подразбиране: UTF-8" + text_git_repository_note: Празно и локално хранилище (например /gitrepo, c:\gitrepo) + text_mercurial_repository_note: Локално хранилище (например /hgrepo, c:\hgrepo) + text_scm_command: SCM команда + text_scm_command_version: ВерÑÐ¸Ñ + text_scm_config: Можете да конфигурирате SCM командите в config/configuration.yml. За да активирате промените, реÑтартирайте Redmine. + text_scm_command_not_available: SCM командата не е налична или доÑтъпна. Проверете конфигурациÑта в админиÑÑ‚Ñ€Ð°Ñ‚Ð¸Ð²Ð½Ð¸Ñ Ð¿Ð°Ð½ÐµÐ». + text_issue_conflict_resolution_overwrite: Прилагане на моите промени (предишните коментари ще бъдат запазени, но нÑкои други промени може да бъдат презапиÑани) + text_issue_conflict_resolution_add_notes: ДобавÑне на моите коментари и отхвърлÑне на другите мои промени + text_issue_conflict_resolution_cancel: ОтхвърлÑне на вÑички мои промени и презареждане на %{link} + text_account_destroy_confirmation: "Сигурен/на ли Ñте, че желаете да продължите?\nВашиÑÑ‚ профил ще бъде премахнат без възможноÑÑ‚ за възÑтановÑване." + text_session_expiration_settings: "Внимание: промÑната на тези уÑтановÑÐ²Ð°Ð½Ð¾Ñ Ð¼Ð¾Ð¶Ðµ да прекрати вÑички активни ÑеÑии, включително и вашата." + text_project_closed: Този проект е затворен и е Ñамо за четене. + text_turning_multiple_off: Ðко забраните възможноÑтта за повече от една ÑтойноÑÑ‚, повечето ÑтойноÑти ще бъдат + премахнати Ñ Ñ†ÐµÐ» да оÑтане Ñамо по една ÑтойноÑÑ‚ за поле. + + 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: ДейноÑти (time tracking) + enumeration_system_activity: СиÑтемна активноÑÑ‚ + description_filter: Филтър + description_search: ТърÑене + description_choose_project: Проекти + description_project_scope: Обхват на търÑенето + description_notes: Бележки + description_message_content: Съдържание на Ñъобщението + description_query_sort_criteria_attribute: Ðтрибут на Ñортиране + description_query_sort_criteria_direction: ПоÑока на Ñортиране + description_user_mail_notification: ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¸Ð·Ð²ÐµÑтиÑта по пощата + description_available_columns: Ðалични колони + description_selected_columns: Избрани колони + description_issue_category_reassign: Изберете ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + description_wiki_subpages_reassign: Изберете нова родителÑка Ñтраница + description_all_columns: Ð’Ñички колони + description_date_range_list: Изберете диапазон от ÑпиÑъка + description_date_range_interval: Изберете диапазон чрез задаване на начална и крайна дати + description_date_from: Въведете начална дата + description_date_to: Въведете крайна дата + text_repository_identifier_info: 'Позволени Ñа малки букви (a-z), цифри, тирета и _.
    ПромÑна Ñлед Ñъздаването му не е възможна.' diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/1840b49390462af0c4e344f45e7614d2e14bd243.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/1840b49390462af0c4e344f45e7614d2e14bd243.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,66 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 SyntaxHighlighting + + class << self + attr_reader :highlighter + + def highlighter=(name) + if name.is_a?(Module) + @highlighter = name + else + @highlighter = const_get(name) + end + end + + def highlight_by_filename(text, filename) + highlighter.highlight_by_filename(text, filename) + rescue + ERB::Util.h(text) + end + + def highlight_by_language(text, language) + highlighter.highlight_by_language(text, language) + rescue + ERB::Util.h(text) + end + end + + module CodeRay + require 'coderay' + + class << self + # Highlights +text+ as the content of +filename+ + # Should not return line numbers nor outer pre tag + def highlight_by_filename(text, filename) + language = ::CodeRay::FileType[filename] + language ? ::CodeRay.scan(text, language).html(:break_lines => true) : ERB::Util.h(text) + end + + # Highlights +text+ using +language+ syntax + # Should not return outer pre tag + def highlight_by_language(text, language) + ::CodeRay.scan(text, language).html(:wrap => :span) + end + end + end + end + + SyntaxHighlighting.highlighter = 'CodeRay' +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/1847d8107fa008755837ae110701a96cb77fd2c3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/1847d8107fa008755837ae110701a96cb77fd2c3.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,23 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssuePriorityCustomField < CustomField + def type_name + :enumeration_issue_priorities + end +end + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/1888c2001e3b4b09dbd1027a98f0a095788cb438.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/1888c2001e3b4b09dbd1027a98f0a095788cb438.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,21 @@ +<%= wiki_page_breadcrumb(@page) %> + +

    <%= h @original_title %>

    + +<%= error_messages_for 'page' %> + +<%= labelled_form_for :wiki_page, @page, + :url => { :action => 'rename' }, + :html => { :method => :post } do |f| %> +
    +

    <%= f.text_field :title, :required => true, :size => 100 %>

    +

    <%= f.check_box :redirect_existing_links %>

    +

    <%= f.select :parent_id, + content_tag('option', '', :value => '') + + wiki_page_options_for_select( + @wiki.pages.includes(:parent).all - @page.self_and_descendants, + @page.parent), + :label => :field_parent_title %>

    +
    +<%= submit_tag l(:button_rename) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/18e8c38332487b5baaa2a884c66c256e8b7f9e68.svn-base --- a/.svn/pristine/18/18e8c38332487b5baaa2a884c66c256e8b7f9e68.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -require File.expand_path('../../../test_helper', __FILE__) - -class ApiTest::HttpBasicLoginWithApiTokenTest < ActionController::IntegrationTest - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows - - def setup - Setting.rest_api_enabled = '1' - Setting.login_required = '1' - end - - def teardown - Setting.rest_api_enabled = '0' - Setting.login_required = '0' - end - - # Using the NewsController because it's a simple API. - context "get /news" do - - context "in :xml format" do - should_allow_http_basic_auth_with_key(:get, "/news.xml") - end - - context "in :json format" do - should_allow_http_basic_auth_with_key(:get, "/news.json") - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/18/18ef0fecd81e51c21a37f84582d5c50bacf3c380.svn-base --- a/.svn/pristine/18/18ef0fecd81e51c21a37f84582d5c50bacf3c380.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,68 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 WikiContentVersionTest < ActiveSupport::TestCase - fixtures :projects, :users, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions - - def setup - end - - def test_destroy - v = WikiContent::Version.find(2) - - assert_difference 'WikiContent::Version.count', -1 do - v.destroy - end - end - - def test_destroy_last_version_should_revert_content - v = WikiContent::Version.find(3) - - assert_no_difference 'WikiPage.count' do - assert_no_difference 'WikiContent.count' do - assert_difference 'WikiContent::Version.count', -1 do - assert v.destroy - end - end - end - c = WikiContent.find(1) - v = c.versions.last - assert_equal 2, c.version - assert_equal v.version, c.version - assert_equal v.comments, c.comments - assert_equal v.text, c.text - assert_equal v.author, c.author - assert_equal v.updated_on, c.updated_on - end - - def test_destroy_all_versions_should_delete_page - WikiContent::Version.find(1).destroy - WikiContent::Version.find(2).destroy - v = WikiContent::Version.find(3) - - assert_difference 'WikiPage.count', -1 do - assert_difference 'WikiContent.count', -1 do - assert_difference 'WikiContent::Version.count', -1 do - assert v.destroy - end - end - end - assert_nil WikiPage.find_by_id(1) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/19/1905c04f7ae0c6da24070033b03c6ae55e5dcb3e.svn-base --- a/.svn/pristine/19/1905c04f7ae0c6da24070033b03c6ae55e5dcb3e.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,95 +0,0 @@ -<%= render :partial => 'action_menu' %> - -

    <%=l(:label_workflow)%>

    - -
    -
      -
    • <%= link_to l(:label_status_transitions), {:action => 'edit', :role_id => @role, :tracker_id => @tracker} %>
    • -
    • <%= link_to l(:label_fields_permissions), {:action => 'permissions', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %>
    • -
    -
    - -

    <%=l(:text_workflow_edit)%>:

    - -<%= form_tag({}, :method => 'get') do %> -

    - - - - - <%= submit_tag l(:button_edit), :name => nil %> - - <%= hidden_field_tag 'used_statuses_only', '0' %> - -

    -<% end %> - -<% if @tracker && @role && @statuses.any? %> - <%= form_tag({}, :id => 'workflow_form' ) do %> - <%= hidden_field_tag 'tracker_id', @tracker.id %> - <%= hidden_field_tag 'role_id', @role.id %> - <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only] %> -
    - - - - - - - - - <% for status in @statuses %> - - <% end %> - - - - - - - <% @fields.each do |field, name| %> - "> - - <% for status in @statuses -%> - - <% end -%> - - <% end %> - <% if @custom_fields.any? %> - - - - <% @custom_fields.each do |field| %> - "> - - <% for status in @statuses -%> - - <% end -%> - - <% end %> - <% end %> - -
    - <%=l(:label_issue_status)%>
    - <%=h status.name %> -
    -   - <%= l(:field_core_fields) %> -
    - <%=h name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %> - - <%= field_permission_tag(@permissions, status, field) %> -
    -   - <%= l(:label_custom_field_plural) %> -
    - <%=h field.name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %> - - <%= field_permission_tag(@permissions, status, field) %> -
    -
    - <%= submit_tag l(:button_save) %> - <% end %> -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/19/1932ee9bec078be0ff0c0caab9f5e072b2fcecac.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/19/1932ee9bec078be0ff0c0caab9f5e072b2fcecac.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,238 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 VersionsControllerTest < ActionController::TestCase + fixtures :projects, :versions, :issues, :users, :roles, :members, + :member_roles, :enabled_modules, :issue_statuses, + :issue_categories + + def setup + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version doesn't appear + assert !assigns(:versions).include?(Version.find(1)) + # Context menu on issues + assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')")) + assert_select "div#sidebar" do + # Links to versions anchors + assert_select 'a[href=?]', '#2.0' + # Links to completed versions in the sidebar + assert_select 'a[href=?]', '/versions/1' + end + end + + def test_index_with_completed_versions + get :index, :project_id => 1, :completed => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version appears + assert assigns(:versions).include?(Version.find(1)) + end + + def test_index_with_tracker_ids + get :index, :project_id => 1, :tracker_ids => [1, 3] + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues_by_version) + assert_nil assigns(:issues_by_version).values.flatten.detect {|issue| issue.tracker_id == 2} + end + + def test_index_showing_subprojects_versions + @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version") + get :index, :project_id => 1, :with_subprojects => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + + assert assigns(:versions).include?(Version.find(4)), "Shared version not found" + assert assigns(:versions).include?(@subproject_version), "Subproject version not found" + end + + def test_index_should_prepend_shared_versions + get :index, :project_id => 1 + assert_response :success + + assert_select '#sidebar' do + assert_select 'a[href=?]', '#2.0', :text => '2.0' + assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0' + end + assert_select '#content' do + assert_select 'a[name=?]', '2.0', :text => '2.0' + assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0' + end + end + + def test_show + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:version) + + assert_tag :tag => 'h2', :content => /1.0/ + end + + def test_show_should_display_nil_counts + with_settings :default_language => 'en' do + get :show, :id => 2, :status_by => 'category' + assert_response :success + assert_select 'div#status_by' do + assert_select 'select[name=status_by]' do + assert_select 'option[value=category][selected=selected]' + end + assert_select 'a', :text => 'none' + end + end + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => '1' + assert_response :success + assert_template 'new' + end + + def test_new_from_issue_form + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => '1' + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_create + @request.session[:user_id] = 2 # manager + assert_difference 'Version.count' do + post :create, :project_id => '1', :version => {:name => 'test_add_version'} + end + assert_redirected_to '/projects/ecookbook/settings/versions' + version = Version.find_by_name('test_add_version') + assert_not_nil version + assert_equal 1, version.project_id + end + + def test_create_from_issue_form + @request.session[:user_id] = 2 + assert_difference 'Version.count' do + xhr :post, :create, :project_id => '1', :version => {:name => 'test_add_version_from_issue_form'} + end + version = Version.find_by_name('test_add_version_from_issue_form') + assert_not_nil version + assert_equal 1, version.project_id + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + assert_include 'test_add_version_from_issue_form', response.body + end + + def test_create_from_issue_form_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + xhr :post, :create, :project_id => '1', :version => {:name => ''} + end + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_close_completed + Version.update_all("status = 'open'") + @request.session[:user_id] = 2 + put :close_completed, :project_id => 'ecookbook' + assert_redirected_to :controller => 'projects', :action => 'settings', + :tab => 'versions', :id => 'ecookbook' + assert_not_nil Version.find_by_status('closed') + end + + def test_post_update + @request.session[:user_id] = 2 + put :update, :id => 2, + :version => {:name => 'New version name', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_redirected_to :controller => 'projects', :action => 'settings', + :tab => 'versions', :id => 'ecookbook' + version = Version.find(2) + assert_equal 'New version name', version.name + assert_equal Date.today, version.effective_date + end + + def test_post_update_with_validation_failure + @request.session[:user_id] = 2 + put :update, :id => 2, + :version => { :name => '', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Version.count', -1 do + delete :destroy, :id => 3 + end + assert_redirected_to :controller => 'projects', :action => 'settings', + :tab => 'versions', :id => 'ecookbook' + assert_nil Version.find_by_id(3) + end + + def test_destroy_version_in_use_should_fail + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + delete :destroy, :id => 2 + end + assert_redirected_to :controller => 'projects', :action => 'settings', + :tab => 'versions', :id => 'ecookbook' + assert flash[:error].match(/Unable to delete version/) + assert Version.find_by_id(2) + end + + def test_issue_status_by + xhr :get, :status_by, :id => 2 + assert_response :success + assert_template 'status_by' + assert_template '_issue_counts' + end + + def test_issue_status_by_status + xhr :get, :status_by, :id => 2, :status_by => 'status' + assert_response :success + assert_template 'status_by' + assert_template '_issue_counts' + assert_include 'Assigned', response.body + assert_include 'Closed', response.body + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/19/194013fb324f2790d7cffbbc3147b775a3c4dd89.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/19/194013fb324f2790d7cffbbc3147b775a3c4dd89.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,809 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 'tcpdf' +require 'fpdf/chinese' +require 'fpdf/japanese' +require 'fpdf/korean' + +if RUBY_VERSION < '1.9' + require 'iconv' +end + +module Redmine + module Export + module PDF + include ActionView::Helpers::TextHelper + include ActionView::Helpers::NumberHelper + include IssuesHelper + + class ITCPDF < TCPDF + include Redmine::I18n + attr_accessor :footer_date + + def initialize(lang, orientation='P') + @@k_path_cache = Rails.root.join('tmp', 'pdf') + FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache) + set_language_if_valid lang + pdf_encoding = l(:general_pdf_encoding).upcase + super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding) + case current_language.to_s.downcase + when 'vi' + @font_for_content = 'DejaVuSans' + @font_for_footer = 'DejaVuSans' + else + case pdf_encoding + when 'UTF-8' + @font_for_content = 'FreeSans' + @font_for_footer = 'FreeSans' + when 'CP949' + extend(PDF_Korean) + AddUHCFont() + @font_for_content = 'UHC' + @font_for_footer = 'UHC' + when 'CP932', 'SJIS', 'SHIFT_JIS' + extend(PDF_Japanese) + AddSJISFont() + @font_for_content = 'SJIS' + @font_for_footer = 'SJIS' + when 'GB18030' + extend(PDF_Chinese) + AddGBFont() + @font_for_content = 'GB' + @font_for_footer = 'GB' + when 'BIG5' + extend(PDF_Chinese) + AddBig5Font() + @font_for_content = 'Big5' + @font_for_footer = 'Big5' + else + @font_for_content = 'Arial' + @font_for_footer = 'Helvetica' + end + end + SetCreator(Redmine::Info.app_name) + SetFont(@font_for_content) + @outlines = [] + @outlineRoot = nil + end + + def SetFontStyle(style, size) + SetFont(@font_for_content, style, size) + end + + def SetTitle(txt) + txt = begin + utf16txt = to_utf16(txt) + hextxt = "" + rescue + txt + end || '' + super(txt) + end + + def textstring(s) + # Format a text string + if s =~ /^\{\{([<>]?)toc\}\}<\/p>/i, '') + html + end + + # Encodes an UTF-8 string to UTF-16BE + def to_utf16(str) + if str.respond_to?(:encode) + str.encode('UTF-16BE') + else + Iconv.conv('UTF-16BE', 'UTF-8', str) + end + end + + def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='') + Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link) + end + + def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1) + MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln) + end + + def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) + @attachments = attachments + writeHTMLCell(w, h, x, y, + fix_text_encoding(formatted_text(txt)), + border, ln, fill) + end + + def getImageFilename(attrname) + # attrname: general_pdf_encoding string file/uri name + atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding)) + if atta + return atta.diskfile + else + return nil + end + end + + def Footer + SetFont(@font_for_footer, 'I', 8) + SetY(-15) + SetX(15) + RDMCell(0, 5, @footer_date, 0, 0, 'L') + SetY(-15) + SetX(-30) + RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C') + end + + def Bookmark(txt, level=0, y=0) + if (y == -1) + y = GetY() + end + @outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k} + end + + def bookmark_title(txt) + txt = begin + utf16txt = to_utf16(txt) + hextxt = "" + rescue + txt + end || '' + end + + def putbookmarks + nb=@outlines.size + return if (nb==0) + lru=[] + level=0 + @outlines.each_with_index do |o, i| + if(o[:l]>0) + parent=lru[o[:l]-1] + #Set parent and last pointers + @outlines[i][:parent]=parent + @outlines[parent][:last]=i + if (o[:l]>level) + #Level increasing: set first pointer + @outlines[parent][:first]=i + end + else + @outlines[i][:parent]=nb + end + if (o[:l]<=level && i>0) + #Set prev and next pointers + prev=lru[o[:l]] + @outlines[prev][:next]=i + @outlines[i][:prev]=prev + end + lru[o[:l]]=i + level=o[:l] + end + #Outline items + n=self.n+1 + @outlines.each_with_index do |o, i| + newobj() + out('<>') + out('endobj') + end + #Outline root + newobj() + @outlineRoot=self.n + out("<>"); + out('endobj'); + end + + def putresources() + super + putbookmarks() + end + + def putcatalog() + super + if(@outlines.size > 0) + out("/Outlines #{@outlineRoot} 0 R"); + out('/PageMode /UseOutlines'); + end + end + end + + # fetch row values + def fetch_row_values(issue, query, level) + query.inline_columns.collect do |column| + s = if column.is_a?(QueryCustomFieldColumn) + cv = issue.visible_custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} + show_value(cv) + else + value = issue.send(column.name) + if column.name == :subject + value = " " * level + value + end + if value.is_a?(Date) + format_date(value) + elsif value.is_a?(Time) + format_time(value) + else + value + end + end + s.to_s + end + end + + # calculate columns width + def calc_col_width(issues, query, table_width, pdf) + # calculate statistics + # by captions + pdf.SetFontStyle('B',8) + col_padding = pdf.GetStringWidth('OO') + col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding} + col_width_max = Array.new(col_width_min) + col_width_avg = Array.new(col_width_min) + word_width_max = query.inline_columns.map {|c| + n = 10 + c.caption.split.each {|w| + x = pdf.GetStringWidth(w) + col_padding + n = x if n < x + } + n + } + + # by properties of issues + pdf.SetFontStyle('',8) + col_padding = pdf.GetStringWidth('OO') + k = 1 + issue_list(issues) {|issue, level| + k += 1 + values = fetch_row_values(issue, query, level) + values.each_with_index {|v,i| + n = pdf.GetStringWidth(v) + col_padding + col_width_max[i] = n if col_width_max[i] < n + col_width_min[i] = n if col_width_min[i] > n + col_width_avg[i] += n + v.split.each {|w| + x = pdf.GetStringWidth(w) + col_padding + word_width_max[i] = x if word_width_max[i] < x + } + } + } + col_width_avg.map! {|x| x / k} + + # calculate columns width + ratio = table_width / col_width_avg.inject(0, :+) + col_width = col_width_avg.map {|w| w * ratio} + + # correct max word width if too many columns + ratio = table_width / word_width_max.inject(0, :+) + word_width_max.map! {|v| v * ratio} if ratio < 1 + + # correct and lock width of some columns + done = 1 + col_fix = [] + col_width.each_with_index do |w,i| + if w > col_width_max[i] + col_width[i] = col_width_max[i] + col_fix[i] = 1 + done = 0 + elsif w < word_width_max[i] + col_width[i] = word_width_max[i] + col_fix[i] = 1 + done = 0 + else + col_fix[i] = 0 + end + end + + # iterate while need to correct and lock coluns width + while done == 0 + # calculate free & locked columns width + done = 1 + fix_col_width = 0 + free_col_width = 0 + col_width.each_with_index do |w,i| + if col_fix[i] == 1 + fix_col_width += w + else + free_col_width += w + end + end + + # calculate column normalizing ratio + if free_col_width == 0 + ratio = table_width / col_width.inject(0, :+) + else + ratio = (table_width - fix_col_width) / free_col_width + end + + # correct columns width + col_width.each_with_index do |w,i| + if col_fix[i] == 0 + col_width[i] = w * ratio + + # check if column width less then max word width + if col_width[i] < word_width_max[i] + col_width[i] = word_width_max[i] + col_fix[i] = 1 + done = 0 + elsif col_width[i] > col_width_max[i] + col_width[i] = col_width_max[i] + col_fix[i] = 1 + done = 0 + end + end + end + end + col_width + end + + def render_table_header(pdf, query, col_width, row_height, table_width) + # headers + pdf.SetFontStyle('B',8) + pdf.SetFillColor(230, 230, 230) + + # render it background to find the max height used + base_x = pdf.GetX + base_y = pdf.GetY + max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) + pdf.Rect(base_x, base_y, table_width, max_height, 'FD'); + pdf.SetXY(base_x, base_y); + + # write the cells on page + issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) + issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width) + pdf.SetY(base_y + max_height); + + # rows + pdf.SetFontStyle('',8) + pdf.SetFillColor(255, 255, 255) + end + + # Returns a PDF string of a list of issues + def issues_to_pdf(issues, project, query) + pdf = ITCPDF.new(current_language, "L") + title = query.new_record? ? l(:label_issue_plural) : query.name + title = "#{project} - #{title}" if project + pdf.SetTitle(title) + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.SetAutoPageBreak(false) + pdf.AddPage("L") + + # Landscape A4 = 210 x 297 mm + page_height = 210 + page_width = 297 + left_margin = 10 + right_margin = 10 + bottom_margin = 20 + row_height = 4 + + # column widths + table_width = page_width - right_margin - left_margin + col_width = [] + unless query.inline_columns.empty? + col_width = calc_col_width(issues, query, table_width, pdf) + table_width = col_width.inject(0, :+) + end + + # use full width if the description is displayed + if table_width > 0 && query.has_column?(:description) + col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width} + table_width = col_width.inject(0, :+) + end + + # title + pdf.SetFontStyle('B',11) + pdf.RDMCell(190,10, title) + pdf.Ln + render_table_header(pdf, query, col_width, row_height, table_width) + previous_group = false + issue_list(issues) do |issue, level| + if query.grouped? && + (group = query.group_by_column.value(issue)) != previous_group + pdf.SetFontStyle('B',10) + group_label = group.blank? ? 'None' : group.to_s.dup + group_label << " (#{query.issue_count_by_group[group]})" + pdf.Bookmark group_label, 0, -1 + pdf.RDMCell(table_width, row_height * 2, group_label, 1, 1, 'L') + pdf.SetFontStyle('',8) + previous_group = group + end + + # fetch row values + col_values = fetch_row_values(issue, query, level) + + # render it off-page to find the max height used + base_x = pdf.GetX + base_y = pdf.GetY + pdf.SetY(2 * page_height) + max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) + pdf.SetXY(base_x, base_y) + + # make new page if it doesn't fit on the current one + space_left = page_height - base_y - bottom_margin + if max_height > space_left + pdf.AddPage("L") + render_table_header(pdf, query, col_width, row_height, table_width) + base_x = pdf.GetX + base_y = pdf.GetY + end + + # write the cells on page + issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) + issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, 0, col_width) + pdf.SetY(base_y + max_height); + + if query.has_column?(:description) && issue.description? + pdf.SetX(10) + pdf.SetAutoPageBreak(true, 20) + pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT") + pdf.SetAutoPageBreak(false) + end + end + + if issues.size == Setting.issues_export_limit.to_i + pdf.SetFontStyle('B',10) + pdf.RDMCell(0, row_height, '...') + end + pdf.Output + end + + # Renders MultiCells and returns the maximum height used + def issues_to_pdf_write_cells(pdf, col_values, col_widths, row_height, head=false) + base_y = pdf.GetY + max_height = row_height + col_values.each_with_index do |column, i| + col_x = pdf.GetX + if head == true + pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1) + else + pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1) + end + max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height + pdf.SetXY(col_x + col_widths[i], base_y); + end + return max_height + end + + # Draw lines to close the row (MultiCell border drawing in not uniform) + # + # parameter "col_id_width" is not used. it is kept for compatibility. + def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y, + col_id_width, col_widths) + col_x = top_x + pdf.Line(col_x, top_y, col_x, lower_y) # id right border + col_widths.each do |width| + col_x += width + pdf.Line(col_x, top_y, col_x, lower_y) # columns right border + end + pdf.Line(top_x, top_y, top_x, lower_y) # left border + pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border + end + + # Returns a PDF string of a single issue + def issue_to_pdf(issue, assoc={}) + pdf = ITCPDF.new(current_language) + pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + pdf.SetFontStyle('B',11) + buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" + pdf.RDMMultiCell(190, 5, buf) + pdf.SetFontStyle('',8) + base_x = pdf.GetX + i = 1 + issue.ancestors.visible.each do |ancestor| + pdf.SetX(base_x + i) + buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" + pdf.RDMMultiCell(190 - i, 5, buf) + i += 1 if i < 35 + end + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) + pdf.SetFontStyle('',8) + pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") + pdf.Ln + + left = [] + left << [l(:field_status), issue.status] + left << [l(:field_priority), issue.priority] + left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') + left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') + left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') + + right = [] + right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') + right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') + right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') + right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') + right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) + + rows = left.size > right.size ? left.size : right.size + while left.size < rows + left << nil + end + while right.size < rows + right << nil + end + + half = (issue.visible_custom_field_values.size / 2.0).ceil + issue.visible_custom_field_values.each_with_index do |custom_value, i| + (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)] + end + + rows = left.size > right.size ? left.size : right.size + rows.times do |i| + item = left[i] + pdf.SetFontStyle('B',9) + pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L") + pdf.SetFontStyle('',9) + pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") + + item = right[i] + pdf.SetFontStyle('B',9) + pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L") + pdf.SetFontStyle('',9) + pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") + pdf.Ln + end + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) + pdf.SetFontStyle('',9) + + # Set resize image scale + pdf.SetImageScale(1.6) + pdf.RDMwriteHTMLCell(35+155, 5, 0, 0, + issue.description.to_s, issue.attachments, "LRB") + + unless issue.leaf? + # for CJK + truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 ) + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR") + pdf.Ln + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}", + :length => truncate_length) + level = 10 if level >= 10 + pdf.SetFontStyle('',8) + pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L") + pdf.SetFontStyle('B',8) + pdf.RDMCell(20,5, child.status.to_s, "R") + pdf.Ln + end + end + + relations = issue.relations.select { |r| r.other_issue(issue).visible? } + unless relations.empty? + # for CJK + truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 ) + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR") + pdf.Ln + relations.each do |relation| + buf = "" + buf += "#{l(relation.label_for(issue))} " + if relation.delay && relation.delay != 0 + buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) " + end + if Setting.cross_project_issue_relations? + buf += "#{relation.other_issue(issue).project} - " + end + buf += "#{relation.other_issue(issue).tracker}" + + " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}" + buf = truncate(buf, :length => truncate_length) + pdf.SetFontStyle('', 8) + pdf.RDMCell(35+155-60, 5, buf, "L") + pdf.SetFontStyle('B',8) + pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "") + pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "") + pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R") + pdf.Ln + end + end + pdf.RDMCell(190,5, "", "T") + pdf.Ln + + if issue.changesets.any? && + User.current.allowed_to?(:view_changesets, issue.project) + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_associated_revisions), "B") + pdf.Ln + for changeset in issue.changesets + pdf.SetFontStyle('B',8) + csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " + csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s + pdf.RDMCell(190, 5, csstr) + pdf.Ln + unless changeset.comments.blank? + pdf.SetFontStyle('',8) + pdf.RDMwriteHTMLCell(190,5,0,0, + changeset.comments.to_s, issue.attachments, "") + end + pdf.Ln + end + end + + if assoc[:journals].present? + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_history), "B") + pdf.Ln + assoc[:journals].each do |journal| + pdf.SetFontStyle('B',8) + title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" + title << " (#{l(:field_private_notes)})" if journal.private_notes? + pdf.RDMCell(190,5, title) + pdf.Ln + pdf.SetFontStyle('I',8) + details_to_strings(journal.visible_details, true).each do |string| + pdf.RDMMultiCell(190,5, "- " + string) + end + if journal.notes? + pdf.Ln unless journal.details.empty? + pdf.SetFontStyle('',8) + pdf.RDMwriteHTMLCell(190,5,0,0, + journal.notes.to_s, issue.attachments, "") + end + pdf.Ln + end + end + + if issue.attachments.any? + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in issue.attachments + pdf.SetFontStyle('',8) + pdf.RDMCell(80,5, attachment.filename) + pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.RDMCell(65,5, attachment.author.name,0,0,"R") + pdf.Ln + end + end + pdf.Output + end + + # Returns a PDF string of a set of wiki pages + def wiki_pages_to_pdf(pages, project) + pdf = ITCPDF.new(current_language) + pdf.SetTitle(project.name) + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190,5, project.name) + pdf.Ln + # Set resize image scale + pdf.SetImageScale(1.6) + pdf.SetFontStyle('',9) + write_page_hierarchy(pdf, pages.group_by(&:parent_id)) + pdf.Output + end + + # Returns a PDF string of a single wiki page + def wiki_page_to_pdf(page, project) + pdf = ITCPDF.new(current_language) + pdf.SetTitle("#{project} - #{page.title}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190,5, + "#{project} - #{page.title} - # #{page.content.version}") + pdf.Ln + # Set resize image scale + pdf.SetImageScale(1.6) + pdf.SetFontStyle('',9) + write_wiki_page(pdf, page) + pdf.Output + end + + def write_page_hierarchy(pdf, pages, node=nil, level=0) + if pages[node] + pages[node].each do |page| + if @new_page + pdf.AddPage + else + @new_page = true + end + pdf.Bookmark page.title, level + write_wiki_page(pdf, page) + write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id] + end + end + end + + def write_wiki_page(pdf, page) + pdf.RDMwriteHTMLCell(190,5,0,0, + page.content.text.to_s, page.attachments, 0) + if page.attachments.any? + pdf.Ln + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in page.attachments + pdf.SetFontStyle('',8) + pdf.RDMCell(80,5, attachment.filename) + pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.RDMCell(65,5, attachment.author.name,0,0,"R") + pdf.Ln + end + end + end + + class RDMPdfEncoding + def self.rdm_from_utf8(txt, encoding) + txt ||= '' + txt = Redmine::CodesetUtil.from_utf8(txt, encoding) + if txt.respond_to?(:force_encoding) + txt.force_encoding('ASCII-8BIT') + end + txt + end + + def self.attach(attachments, filename, encoding) + filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding) + atta = nil + if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i + atta = Attachment.latest_attach(attachments, filename_utf8) + end + if atta && atta.readable? && atta.visible? + return atta + else + return nil + end + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/19/1959b21aed080e49e7eb19db5f9d559c39f78f7a.svn-base --- a/.svn/pristine/19/1959b21aed080e49e7eb19db5f9d559c39f78f7a.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,154 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'journals_controller' - -# Re-raise errors caught by the controller. -class JournalsController; def rescue_action(e) raise e end; end - -class JournalsControllerTest < ActionController::TestCase - fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules, - :trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects - - def setup - @controller = JournalsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - end - - def test_index - get :index, :project_id => 1 - assert_response :success - assert_not_nil assigns(:journals) - assert_equal 'application/atom+xml', @response.content_type - end - - def test_index_should_return_privates_notes_with_permission_only - journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) - @request.session[:user_id] = 2 - - get :index, :project_id => 1 - assert_response :success - assert_include journal, assigns(:journals) - - Role.find(1).remove_permission! :view_private_notes - get :index, :project_id => 1 - assert_response :success - assert_not_include journal, assigns(:journals) - end - - def test_diff - get :diff, :id => 3, :detail_id => 4 - assert_response :success - assert_template 'diff' - - assert_tag 'span', - :attributes => {:class => 'diff_out'}, - :content => /removed/ - assert_tag 'span', - :attributes => {:class => 'diff_in'}, - :content => /added/ - end - - def test_reply_to_issue - @request.session[:user_id] = 2 - xhr :get, :new, :id => 6 - assert_response :success - assert_template 'new' - assert_equal 'text/javascript', response.content_type - assert_include '> This is an issue', response.body - end - - def test_reply_to_issue_without_permission - @request.session[:user_id] = 7 - xhr :get, :new, :id => 6 - assert_response 403 - end - - def test_reply_to_note - @request.session[:user_id] = 2 - xhr :get, :new, :id => 6, :journal_id => 4 - assert_response :success - assert_template 'new' - assert_equal 'text/javascript', response.content_type - assert_include '> A comment with a private version', response.body - end - - def test_reply_to_private_note_should_fail_without_permission - journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) - @request.session[:user_id] = 2 - - xhr :get, :new, :id => 2, :journal_id => journal.id - assert_response :success - assert_template 'new' - assert_equal 'text/javascript', response.content_type - assert_include '> Privates notes', response.body - - Role.find(1).remove_permission! :view_private_notes - xhr :get, :new, :id => 2, :journal_id => journal.id - assert_response 404 - end - - def test_edit_xhr - @request.session[:user_id] = 1 - xhr :get, :edit, :id => 2 - assert_response :success - assert_template 'edit' - assert_equal 'text/javascript', response.content_type - assert_include 'textarea', response.body - end - - def test_edit_private_note_should_fail_without_permission - journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) - @request.session[:user_id] = 2 - Role.find(1).add_permission! :edit_issue_notes - - xhr :get, :edit, :id => journal.id - assert_response :success - assert_template 'edit' - assert_equal 'text/javascript', response.content_type - assert_include 'textarea', response.body - - Role.find(1).remove_permission! :view_private_notes - xhr :get, :edit, :id => journal.id - assert_response 404 - end - - def test_update_xhr - @request.session[:user_id] = 1 - xhr :post, :edit, :id => 2, :notes => 'Updated notes' - assert_response :success - assert_template 'update' - assert_equal 'text/javascript', response.content_type - assert_equal 'Updated notes', Journal.find(2).notes - assert_include 'journal-2-notes', response.body - end - - def test_update_xhr_with_empty_notes_should_delete_the_journal - @request.session[:user_id] = 1 - assert_difference 'Journal.count', -1 do - xhr :post, :edit, :id => 2, :notes => '' - assert_response :success - assert_template 'update' - assert_equal 'text/javascript', response.content_type - end - assert_nil Journal.find_by_id(2) - assert_include 'change-2', response.body - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/19/196c7c4e4ef7e50cda7944736d264f90424a0cab.svn-base --- a/.svn/pristine/19/196c7c4e4ef7e50cda7944736d264f90424a0cab.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,145 +0,0 @@ -#!/usr/bin/env ruby - -require 'net/http' -require 'net/https' -require 'uri' -require 'optparse' - -module Net - class HTTPS < HTTP - def self.post_form(url, params, headers, options={}) - request = Post.new(url.path) - request.form_data = params - request.initialize_http_header(headers) - request.basic_auth url.user, url.password if url.user - http = new(url.host, url.port) - http.use_ssl = (url.scheme == 'https') - if options[:no_check_certificate] - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - http.start {|h| h.request(request) } - end - end -end - -class RedmineMailHandler - VERSION = '0.2.1' - - attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :no_permission_check, :url, :key, :no_check_certificate - - def initialize - self.issue_attributes = {} - - optparse = OptionParser.new do |opts| - opts.banner = "Usage: rdm-mailhandler.rb [options] --url= --key=" - opts.separator("") - opts.separator("Reads an email from standard input and forward it to a Redmine server through a HTTP request.") - opts.separator("") - opts.separator("Required arguments:") - opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v} - opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v} - opts.separator("") - opts.separator("General options:") - opts.on("--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") {|v| self.unknown_user = v} - opts.on("--no-permission-check", "disable permission checking when receiving", - "the email") {self.no_permission_check = '1'} - opts.on("--key-file FILE", "path to a file that contains the Redmine", - "API key (use this option instead of --key", - "if you don't the key to appear in the", - "command line)") {|v| read_key_from_file(v)} - opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true} - opts.on("-h", "--help", "show this help") {puts opts; exit 1} - opts.on("-v", "--verbose", "show extra information") {self.verbose = true} - opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit} - opts.separator("") - opts.separator("Issue attributes control options:") - opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v} - opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v} - opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v} - opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v} - opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v} - opts.on("-o", "--allow-override ATTRS", "allow email content to override attributes", - "specified by previous options", - "ATTRS is a comma separated list of attributes") {|v| self.allow_override = v} - opts.separator("") - opts.separator("Examples:") - opts.separator("No project specified. Emails MUST contain the 'Project' keyword:") - opts.separator(" rdm-mailhandler.rb --url http://redmine.domain.foo --key secret") - opts.separator("") - opts.separator("Fixed project and default tracker specified, but emails can override") - opts.separator("both tracker and priority attributes using keywords:") - opts.separator(" rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\") - opts.separator(" --project foo \\") - opts.separator(" --tracker bug \\") - opts.separator(" --allow-override tracker,priority") - - opts.summary_width = 27 - end - optparse.parse! - - unless url && key - puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help." - exit 1 - end - end - - def submit(email) - uri = url.gsub(%r{/*$}, '') + '/mail_handler' - - headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" } - - data = { 'key' => key, 'email' => email, - 'allow_override' => allow_override, - 'unknown_user' => unknown_user, - 'no_permission_check' => no_permission_check} - issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } - - debug "Posting to #{uri}..." - response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate) - debug "Response received: #{response.code}" - - case response.code.to_i - when 403 - warn "Request was denied by your Redmine server. " + - "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key." - return 77 - when 422 - warn "Request was denied by your Redmine server. " + - "Possible reasons: email is sent from an invalid email address or is missing some information." - return 77 - when 400..499 - warn "Request was denied by your Redmine server (#{response.code})." - return 77 - when 500..599 - warn "Failed to contact your Redmine server (#{response.code})." - return 75 - when 201 - debug "Proccessed successfully" - return 0 - else - return 1 - end - end - - private - - def debug(msg) - puts msg if verbose - end - - def read_key_from_file(filename) - begin - self.key = File.read(filename).strip - rescue Exception => e - $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}" - exit 1 - end - end -end - -handler = RedmineMailHandler.new -exit(handler.submit(STDIN.read)) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/19/19c2933293d5536cb86e9ab59b2a501d3507d1a0.svn-base --- a/.svn/pristine/19/19c2933293d5536cb86e9ab59b2a501d3507d1a0.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingWikiTest < ActionController::IntegrationTest - def test_wiki_matching - assert_routing( - { :method => 'get', :path => "/projects/567/wiki" }, - { :controller => 'wiki', :action => 'show', :project_id => '567' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/lalala" }, - { :controller => 'wiki', :action => 'show', :project_id => '567', - :id => 'lalala' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/lalala.pdf" }, - { :controller => 'wiki', :action => 'show', :project_id => '567', - :id => 'lalala', :format => 'pdf' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/diff" }, - { :controller => 'wiki', :action => 'diff', :project_id => '1', - :id => 'CookBook_documentation' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2" }, - { :controller => 'wiki', :action => 'show', :project_id => '1', - :id => 'CookBook_documentation', :version => '2' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/diff" }, - { :controller => 'wiki', :action => 'diff', :project_id => '1', - :id => 'CookBook_documentation', :version => '2' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/annotate" }, - { :controller => 'wiki', :action => 'annotate', :project_id => '1', - :id => 'CookBook_documentation', :version => '2' } - ) - end - - def test_wiki_misc - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/date_index" }, - { :controller => 'wiki', :action => 'date_index', :project_id => '567' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/export" }, - { :controller => 'wiki', :action => 'export', :project_id => '567' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/export.pdf" }, - { :controller => 'wiki', :action => 'export', :project_id => '567', :format => 'pdf' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/index" }, - { :controller => 'wiki', :action => 'index', :project_id => '567' } - ) - end - - def test_wiki_resources - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/my_page/edit" }, - { :controller => 'wiki', :action => 'edit', :project_id => '567', - :id => 'my_page' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/history" }, - { :controller => 'wiki', :action => 'history', :project_id => '1', - :id => 'CookBook_documentation' } - ) - assert_routing( - { :method => 'get', :path => "/projects/22/wiki/ladida/rename" }, - { :controller => 'wiki', :action => 'rename', :project_id => '22', - :id => 'ladida' } - ) - ["post", "put"].each do |method| - assert_routing( - { :method => method, :path => "/projects/567/wiki/CookBook_documentation/preview" }, - { :controller => 'wiki', :action => 'preview', :project_id => '567', - :id => 'CookBook_documentation' } - ) - end - assert_routing( - { :method => 'post', :path => "/projects/22/wiki/ladida/rename" }, - { :controller => 'wiki', :action => 'rename', :project_id => '22', - :id => 'ladida' } - ) - assert_routing( - { :method => 'post', :path => "/projects/22/wiki/ladida/protect" }, - { :controller => 'wiki', :action => 'protect', :project_id => '22', - :id => 'ladida' } - ) - assert_routing( - { :method => 'post', :path => "/projects/22/wiki/ladida/add_attachment" }, - { :controller => 'wiki', :action => 'add_attachment', :project_id => '22', - :id => 'ladida' } - ) - assert_routing( - { :method => 'put', :path => "/projects/567/wiki/my_page" }, - { :controller => 'wiki', :action => 'update', :project_id => '567', - :id => 'my_page' } - ) - assert_routing( - { :method => 'delete', :path => "/projects/22/wiki/ladida" }, - { :controller => 'wiki', :action => 'destroy', :project_id => '22', - :id => 'ladida' } - ) - assert_routing( - { :method => 'delete', :path => "/projects/22/wiki/ladida/3" }, - { :controller => 'wiki', :action => 'destroy_version', :project_id => '22', - :id => 'ladida', :version => '3' } - ) - end - - def test_api - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/my_page.xml" }, - { :controller => 'wiki', :action => 'show', :project_id => '567', - :id => 'my_page', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/my_page.json" }, - { :controller => 'wiki', :action => 'show', :project_id => '567', - :id => 'my_page', :format => 'json' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.xml" }, - { :controller => 'wiki', :action => 'show', :project_id => '1', - :id => 'CookBook_documentation', :version => '2', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.json" }, - { :controller => 'wiki', :action => 'show', :project_id => '1', - :id => 'CookBook_documentation', :version => '2', :format => 'json' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/index.xml" }, - { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/projects/567/wiki/index.json" }, - { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'json' } - ) - assert_routing( - { :method => 'put', :path => "/projects/567/wiki/my_page.xml" }, - { :controller => 'wiki', :action => 'update', :project_id => '567', - :id => 'my_page', :format => 'xml' } - ) - assert_routing( - { :method => 'put', :path => "/projects/567/wiki/my_page.json" }, - { :controller => 'wiki', :action => 'update', :project_id => '567', - :id => 'my_page', :format => 'json' } - ) - assert_routing( - { :method => 'delete', :path => "/projects/567/wiki/my_page.xml" }, - { :controller => 'wiki', :action => 'destroy', :project_id => '567', - :id => 'my_page', :format => 'xml' } - ) - assert_routing( - { :method => 'delete', :path => "/projects/567/wiki/my_page.json" }, - { :controller => 'wiki', :action => 'destroy', :project_id => '567', - :id => 'my_page', :format => 'json' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a021d35a6845d33a0f5158ab203eef12d7b8200.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a021d35a6845d33a0f5158ab203eef12d7b8200.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,67 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::Views::Builders::XmlTest < ActiveSupport::TestCase + + def test_hash + assert_xml_output('Ryan32') do |b| + b.person do + b.name 'Ryan' + b.age 32 + end + end + end + + def test_array + assert_xml_output('') do |b| + b.array :books do |b| + b.book :title => 'Book 1' + b.book :title => 'Book 2' + end + end + end + + def test_array_with_content_tags + assert_xml_output('Book 1Book 2') do |b| + b.array :books do |b| + b.book 'Book 1', :author => 'B. Smith' + b.book 'Book 2', :author => 'G. Cooper' + end + end + end + + def test_nested_arrays + assert_xml_output('B. SmithG. Cooper') do |b| + b.array :books do |books| + books.book do |book| + book.array :authors do |authors| + authors.author 'B. Smith' + authors.author 'G. Cooper' + end + end + end + end + end + + def assert_xml_output(expected, &block) + builder = Redmine::Views::Builders::Xml.new(ActionDispatch::TestRequest.new, ActionDispatch::TestResponse.new) + block.call(builder) + assert_equal('' + expected, builder.output) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a0df8758a6961e9243b5416c8d2489913c4510a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a0df8758a6961e9243b5416c8d2489913c4510a.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,96 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 EnumerationsController < ApplicationController + layout 'admin' + + before_filter :require_admin, :except => :index + before_filter :require_admin_or_api_request, :only => :index + before_filter :build_new_enumeration, :only => [:new, :create] + before_filter :find_enumeration, :only => [:edit, :update, :destroy] + accept_api_auth :index + + helper :custom_fields + + def index + respond_to do |format| + format.html + format.api { + @klass = Enumeration.get_subclass(params[:type]) + if @klass + @enumerations = @klass.shared.sorted.all + else + render_404 + end + } + end + end + + def new + end + + def create + if request.post? && @enumeration.save + flash[:notice] = l(:notice_successful_create) + redirect_to enumerations_path + else + render :action => 'new' + end + end + + def edit + end + + def update + if request.put? && @enumeration.update_attributes(params[:enumeration]) + flash[:notice] = l(:notice_successful_update) + redirect_to enumerations_path + else + render :action => 'edit' + end + end + + def destroy + if !@enumeration.in_use? + # No associated objects + @enumeration.destroy + redirect_to enumerations_path + return + elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i)) + @enumeration.destroy(reassign_to) + redirect_to enumerations_path + return + end + @enumerations = @enumeration.class.system.all - [@enumeration] + end + + private + + def build_new_enumeration + class_name = params[:enumeration] && params[:enumeration][:type] || params[:type] + @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration]) + if @enumeration.nil? + render_404 + end + end + + def find_enumeration + @enumeration = Enumeration.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a39e16716c41d7a0a0fcc3821bef50938cf1bfa.svn-base --- a/.svn/pristine/1a/1a39e16716c41d7a0a0fcc3821bef50938cf1bfa.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 GroupCustomField < CustomField - def type_name - :label_group_plural - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a713b2971dc5e1253400d3e2c453e825220dc14.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a713b2971dc5e1253400d3e2c453e825220dc14.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1119 @@ +# Redmine EU language +# Author: Ales Zabala Alava (Shagi), +# 2010-01-25 +# Distributed under the same terms as the Redmine itself. +eu: + direction: ltr + 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: [Igandea, Astelehena, Asteartea, Asteazkena, Osteguna, Ostirala, Larunbata] + abbr_day_names: [Ig., Al., Ar., Az., Og., Or., La.] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Urtarrila, Otsaila, Martxoa, Apirila, Maiatza, Ekaina, Uztaila, Abuztua, Iraila, Urria, Azaroa, Abendua] + abbr_month_names: [~, Urt, Ots, Mar, Api, Mai, Eka, Uzt, Abu, Ira, Urr, Aza, Abe] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y/%m/%d %H:%M" + time: "%H:%M" + short: "%b %d %H:%M" + long: "%Y %B %d %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "minutu erdi" + less_than_x_seconds: + one: "segundu bat baino gutxiago" + other: "%{count} segundu baino gutxiago" + x_seconds: + one: "segundu 1" + other: "%{count} segundu" + less_than_x_minutes: + one: "minutu bat baino gutxiago" + other: "%{count} minutu baino gutxiago" + x_minutes: + one: "minutu 1" + other: "%{count} minutu" + about_x_hours: + one: "ordu 1 inguru" + other: "%{count} ordu inguru" + x_hours: + one: "ordu 1" + other: "%{count} ordu" + x_days: + one: "egun 1" + other: "%{count} egun" + about_x_months: + one: "hilabete 1 inguru" + other: "%{count} hilabete inguru" + x_months: + one: "hilabete 1" + other: "%{count} hilabete" + about_x_years: + one: "urte 1 inguru" + other: "%{count} urte inguru" + over_x_years: + one: "urte 1 baino gehiago" + other: "%{count} urte baino gehiago" + almost_x_years: + one: "ia urte 1" + other: "ia %{count} urte" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Byte" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "eta" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "Errore batek %{model} hau godetzea galarazi du." + other: "%{count} errorek %{model} hau gordetzea galarazi dute." + messages: + inclusion: "ez dago zerrendan" + exclusion: "erreserbatuta dago" + invalid: "baliogabea da" + confirmation: "ez du berrespenarekin bat egiten" + accepted: "onartu behar da" + empty: "ezin da hutsik egon" + blank: "ezin da hutsik egon" + too_long: "luzeegia da (maximoa %{count} karaktere dira)" + too_short: "laburregia da (minimoa %{count} karaktere dira)" + wrong_length: "luzera ezegokia da (%{count} karakter izan beharko litzake)" + taken: "dagoeneko hartuta dago" + not_a_number: "ez da zenbaki bat" + not_a_date: "ez da baliozko data" + greater_than: "%{count} baino handiagoa izan behar du" + greater_than_or_equal_to: "%{count} edo handiagoa izan behar du" + equal_to: "%{count} izan behar du" + less_than: "%{count} baino gutxiago izan behar du" + less_than_or_equal_to: "%{count} edo gutxiago izan behar du" + odd: "bakoitia izan behar du" + even: "bikoitia izan behar du" + greater_than_start_date: "hasiera data baino handiagoa izan behar du" + not_same_project: "ez dago proiektu berdinean" + circular_dependency: "Erlazio honek mendekotasun zirkular bat sortuko luke" + cant_link_an_issue_with_a_descendant: "Zeregin bat ezin da bere azpiataza batekin estekatu." + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Hautatu mesedez + + general_text_No: 'Ez' + general_text_Yes: 'Bai' + general_text_no: 'ez' + general_text_yes: 'bai' + general_lang_name: 'Euskara' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Kontua ongi eguneratu da. + notice_account_invalid_creditentials: Erabiltzaile edo pasahitz ezegokia + notice_account_password_updated: Pasahitza ongi eguneratu da. + notice_account_wrong_password: Pasahitz ezegokia. + notice_account_register_done: Kontua ongi sortu da. Kontua gaitzeko klikatu epostan adierazi zaizun estekan. + notice_account_unknown_email: Erabiltzaile ezezaguna. + notice_can_t_change_password: Kontu honek kanpoko autentikazio bat erabiltzen du. Ezinezkoa da pasahitza aldatzea. + notice_account_lost_email_sent: Pasahitz berria aukeratzeko jarraibideak dituen eposta bat bidali zaizu. + notice_account_activated: Zure kontua gaituta dago. Orain saioa has dezakezu + notice_successful_create: Sortze arrakastatsua. + notice_successful_update: Eguneratze arrakastatsua. + notice_successful_delete: Ezabaketa arrakastatsua. + notice_successful_connection: Konexio arrakastatsua. + notice_file_not_found: Atzitu nahi duzun orria ez da exisitzen edo ezabatua izan da. + notice_locking_conflict: Beste erabiltzaile batek datuak eguneratu ditu. + notice_not_authorized: Ez duzu orri hau atzitzeko baimenik. + notice_email_sent: "%{value} helbidera eposta bat bidali da" + notice_email_error: "Errorea eposta bidaltzean (%{value})" + notice_feeds_access_key_reseted: Zure Atom atzipen giltza berrezarri da. + notice_api_access_key_reseted: Zure API atzipen giltza berrezarri da. + notice_failed_to_save_issues: "Hautatutako %{total} zereginetatik %{count} ezin izan dira konpondu: %{ids}." + notice_no_issue_selected: "Ez da zereginik hautatu! Mesedez, editatu nahi dituzun arazoak markatu." + notice_account_pending: "Zure kontua sortu da, orain kudeatzailearen onarpenaren zain dago." + notice_default_data_loaded: Lehenetsitako konfigurazioa ongi kargatu da. + notice_unable_delete_version: Ezin da bertsioa ezabatu. + notice_issue_done_ratios_updated: Burututako zereginen erlazioa eguneratu da. + + error_can_t_load_default_data: "Ezin izan da lehenetsitako konfigurazioa kargatu: %{value}" + error_scm_not_found: "Sarrera edo berrikuspena ez da biltegian topatu." + error_scm_command_failed: "Errorea gertatu da biltegia atzitzean: %{value}" + error_scm_annotate: "Sarrera ez da existitzen edo ezin da anotatu." + error_issue_not_found_in_project: 'Zeregina ez da topatu edo ez da proiektu honetakoa' + error_no_tracker_in_project: 'Proiektu honek ez du aztarnaririk esleituta. Mesedez egiaztatu Proiektuaren ezarpenak.' + error_no_default_issue_status: 'Zereginek ez dute lehenetsitako egoerarik. Mesedez egiaztatu zure konfigurazioa ("Kudeaketa -> Arazoen egoerak" atalera joan).' + error_can_not_reopen_issue_on_closed_version: 'Itxitako bertsio batera esleitutako zereginak ezin dira berrireki' + error_can_not_archive_project: Proiektu hau ezin da artxibatu + error_issue_done_ratios_not_updated: "Burututako zereginen erlazioa ez da eguneratu." + error_workflow_copy_source: 'Mesedez hautatu iturburuko aztarnari edo rola' + error_workflow_copy_target: 'Mesedez hautatu helburuko aztarnari(ak) edo rola(k)' + + warning_attachments_not_saved: "%{count} fitxategi ezin izan d(ir)a gorde." + + mail_subject_lost_password: "Zure %{value} pasahitza" + mail_body_lost_password: 'Zure pasahitza aldatzeko hurrengo estekan klikatu:' + mail_subject_register: "Zure %{value} kontuaren gaitzea" + mail_body_register: 'Zure kontua gaitzeko hurrengo estekan klikatu:' + mail_body_account_information_external: "Zure %{value} kontua erabil dezakezu saioa hasteko." + mail_body_account_information: Zure kontuaren informazioa + mail_subject_account_activation_request: "%{value} kontu gaitzeko eskaera" + mail_body_account_activation_request: "Erabiltzaile berri bat (%{value}) erregistratu da. Kontua zure onarpenaren zain dago:" + mail_subject_reminder: "%{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a" + mail_body_reminder: "Zuri esleituta dauden %{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a:" + mail_subject_wiki_content_added: "'%{id}' wiki orria gehitu da" + mail_body_wiki_content_added: "%{author}-(e)k '%{id}' wiki orria gehitu du." + mail_subject_wiki_content_updated: "'%{id}' wiki orria eguneratu da" + mail_body_wiki_content_updated: "%{author}-(e)k '%{id}' wiki orria eguneratu du." + + + field_name: Izena + field_description: Deskribapena + field_summary: Laburpena + field_is_required: Beharrezkoa + field_firstname: Izena + field_lastname: Abizenak + field_mail: Eposta + field_filename: Fitxategia + field_filesize: Tamaina + field_downloads: Deskargak + field_author: Egilea + field_created_on: Sortuta + field_updated_on: Eguneratuta + field_field_format: Formatua + field_is_for_all: Proiektu guztietarako + field_possible_values: Balio posibleak + field_regexp: Expresio erregularra + field_min_length: Luzera minimoa + field_max_length: Luzera maxioma + field_value: Balioa + field_category: Kategoria + field_title: Izenburua + field_project: Proiektua + field_issue: Zeregina + field_status: Egoera + field_notes: Oharrak + field_is_closed: Itxitako arazoa + field_is_default: Lehenetsitako balioa + field_tracker: Aztarnaria + field_subject: Gaia + field_due_date: Amaiera data + field_assigned_to: Esleituta + field_priority: Lehentasuna + field_fixed_version: Helburuko bertsioa + field_user: Erabiltzilea + field_role: Rola + field_homepage: Orri nagusia + field_is_public: Publikoa + field_parent: "Honen azpiproiektua:" + field_is_in_roadmap: Arazoak ibilbide-mapan erakutsi + field_login: Erabiltzaile izena + field_mail_notification: Eposta jakinarazpenak + field_admin: Kudeatzailea + field_last_login_on: Azken konexioa + field_language: Hizkuntza + field_effective_date: Data + field_password: Pasahitza + field_new_password: Pasahitz berria + field_password_confirmation: Berrespena + field_version: Bertsioa + field_type: Mota + field_host: Ostalaria + field_port: Portua + field_account: Kontua + field_base_dn: Base DN + field_attr_login: Erabiltzaile atributua + field_attr_firstname: Izena atributua + field_attr_lastname: Abizenak atributua + field_attr_mail: Eposta atributua + field_onthefly: Zuzeneko erabiltzaile sorrera + field_start_date: Hasiera + field_done_ratio: Egindako % + field_auth_source: Autentikazio modua + field_hide_mail: Nire eposta helbidea ezkutatu + field_comments: Iruzkina + field_url: URL + field_start_page: Hasierako orria + field_subproject: Azpiproiektua + field_hours: Ordu + field_activity: Jarduera + field_spent_on: Data + field_identifier: Identifikatzailea + field_is_filter: Iragazki moduan erabilita + field_issue_to: Erlazionatutako zereginak + field_delay: Atzerapena + field_assignable: Arazoak rol honetara esleitu daitezke + field_redirect_existing_links: Existitzen diren estekak berbideratu + field_estimated_hours: Estimatutako denbora + field_column_names: Zutabeak + field_time_zone: Ordu zonaldea + field_searchable: Bilagarria + field_default_value: Lehenetsitako balioa + field_comments_sorting: Iruzkinak erakutsi + field_parent_title: Orri gurasoa + field_editable: Editagarria + field_watcher: Behatzailea + field_identity_url: OpenID URLa + field_content: Edukia + field_group_by: Emaitzak honegatik taldekatu + field_sharing: Partekatzea + + setting_app_title: Aplikazioaren izenburua + setting_app_subtitle: Aplikazioaren azpizenburua + setting_welcome_text: Ongietorriko testua + setting_default_language: Lehenetsitako hizkuntza + setting_login_required: Autentikazioa derrigorrezkoa + setting_self_registration: Norberak erregistratu + setting_attachment_max_size: Eranskinen tamaina max. + setting_issues_export_limit: Zereginen esportatze limitea + setting_mail_from: Igorlearen eposta helbidea + setting_bcc_recipients: Hartzaileak ezkutuko kopian (bcc) + setting_plain_text_mail: Testu soileko epostak (HTML-rik ez) + setting_host_name: Ostalari izena eta bidea + setting_text_formatting: Testu formatua + setting_wiki_compression: Wikiaren historia konprimitu + setting_feeds_limit: Jarioaren edukiera limitea + setting_default_projects_public: Proiektu berriak defektuz publikoak dira + setting_autofetch_changesets: Commit-ak automatikoki hartu + setting_sys_api_enabled: Biltegien kudeaketarako WS gaitu + setting_commit_ref_keywords: Erreferentzien gako-hitzak + setting_commit_fix_keywords: Konpontze gako-hitzak + setting_autologin: Saioa automatikoki hasi + setting_date_format: Data formatua + setting_time_format: Ordu formatua + setting_cross_project_issue_relations: Zereginak proiektuen artean erlazionatzea baimendu + setting_issue_list_default_columns: Zereginen zerrendan defektuz ikusten diren zutabeak + setting_emails_footer: Eposten oina + setting_protocol: Protokoloa + setting_per_page_options: Orriko objektuen aukerak + setting_user_format: Erabiltzaileak erakusteko formatua + setting_activity_days_default: Proiektuen jardueran erakusteko egunak + setting_display_subprojects_issues: Azpiproiektuen zereginak proiektu nagusian erakutsi defektuz + setting_enabled_scm: Gaitutako IKKak + setting_mail_handler_body_delimiters: "Lerro hauteko baten ondoren epostak moztu" + setting_mail_handler_api_enabled: Sarrerako epostentzako WS gaitu + setting_mail_handler_api_key: API giltza + setting_sequential_project_identifiers: Proiektuen identifikadore sekuentzialak sortu + setting_gravatar_enabled: Erabili Gravatar erabiltzaile ikonoak + setting_gravatar_default: Lehenetsitako Gravatar irudia + setting_diff_max_lines_displayed: Erakutsiko diren diff lerro kopuru maximoa + setting_file_max_size_displayed: Barnean erakuzten diren testu fitxategien tamaina maximoa + setting_repository_log_display_limit: Egunkari fitxategian erakutsiko diren berrikuspen kopuru maximoa. + setting_openid: Baimendu OpenID saio hasiera eta erregistatzea + setting_password_min_length: Pasahitzen luzera minimoa + setting_new_project_user_role_id: Proiektu berriak sortzerakoan kudeatzaile ez diren erabiltzaileei esleitutako rola + setting_default_projects_modules: Proiektu berrientzako defektuz gaitutako moduluak + setting_issue_done_ratio: "Zereginen burututako tasa kalkulatzean erabili:" + setting_issue_done_ratio_issue_field: Zeregin eremua erabili + setting_issue_done_ratio_issue_status: Zeregin egoera erabili + setting_start_of_week: "Egutegiak noiz hasi:" + setting_rest_api_enabled: Gaitu REST web zerbitzua + + permission_add_project: Proiektua sortu + permission_add_subprojects: Azpiproiektuak sortu + permission_edit_project: Proiektua editatu + permission_select_project_modules: Proiektuaren moduluak hautatu + permission_manage_members: Kideak kudeatu + permission_manage_versions: Bertsioak kudeatu + permission_manage_categories: Arazoen kategoriak kudeatu + permission_view_issues: Zereginak ikusi + permission_add_issues: Zereginak gehitu + permission_edit_issues: Zereginak aldatu + permission_manage_issue_relations: Zereginen erlazioak kudeatu + permission_add_issue_notes: Oharrak gehitu + permission_edit_issue_notes: Oharrak aldatu + permission_edit_own_issue_notes: Nork bere oharrak aldatu + permission_move_issues: Zereginak mugitu + permission_delete_issues: Zereginak ezabatu + permission_manage_public_queries: Galdera publikoak kudeatu + permission_save_queries: Galderak gorde + permission_view_gantt: Gantt grafikoa ikusi + permission_view_calendar: Egutegia ikusi + permission_view_issue_watchers: Behatzaileen zerrenda ikusi + permission_add_issue_watchers: Behatzaileak gehitu + permission_delete_issue_watchers: Behatzaileak ezabatu + permission_log_time: Igarotako denbora erregistratu + permission_view_time_entries: Igarotako denbora ikusi + permission_edit_time_entries: Denbora egunkariak editatu + permission_edit_own_time_entries: Nork bere denbora egunkariak editatu + permission_manage_news: Berriak kudeatu + permission_comment_news: Berrien iruzkinak egin + permission_view_documents: Dokumentuak ikusi + permission_manage_files: Fitxategiak kudeatu + permission_view_files: Fitxategiak ikusi + permission_manage_wiki: Wikia kudeatu + permission_rename_wiki_pages: Wiki orriak berrizendatu + permission_delete_wiki_pages: Wiki orriak ezabatu + permission_view_wiki_pages: Wikia ikusi + permission_view_wiki_edits: Wikiaren historia ikusi + permission_edit_wiki_pages: Wiki orriak editatu + permission_delete_wiki_pages_attachments: Eranskinak ezabatu + permission_protect_wiki_pages: Wiki orriak babestu + permission_manage_repository: Biltegiak kudeatu + permission_browse_repository: Biltegia arakatu + permission_view_changesets: Aldaketak ikusi + permission_commit_access: Commit atzipena + permission_manage_boards: Foroak kudeatu + permission_view_messages: Mezuak ikusi + permission_add_messages: Mezuak bidali + permission_edit_messages: Mezuak aldatu + permission_edit_own_messages: Nork bere mezuak aldatu + permission_delete_messages: Mezuak ezabatu + permission_delete_own_messages: Nork bere mezuak ezabatu + + project_module_issue_tracking: Zereginen jarraipena + project_module_time_tracking: Denbora jarraipena + project_module_news: Berriak + project_module_documents: Dokumentuak + project_module_files: Fitxategiak + project_module_wiki: Wiki + project_module_repository: Biltegia + project_module_boards: Foroak + + label_user: Erabiltzailea + label_user_plural: Erabiltzaileak + label_user_new: Erabiltzaile berria + label_user_anonymous: Ezezaguna + label_project: Proiektua + label_project_new: Proiektu berria + label_project_plural: Proiektuak + label_x_projects: + zero: proiekturik ez + one: proiektu bat + other: "%{count} proiektu" + label_project_all: Proiektu guztiak + label_project_latest: Azken proiektuak + label_issue: Zeregina + label_issue_new: Zeregin berria + label_issue_plural: Zereginak + label_issue_view_all: Zeregin guztiak ikusi + label_issues_by: "Zereginak honengatik: %{value}" + label_issue_added: Zeregina gehituta + label_issue_updated: Zeregina eguneratuta + label_document: Dokumentua + label_document_new: Dokumentu berria + label_document_plural: Dokumentuak + label_document_added: Dokumentua gehituta + label_role: Rola + label_role_plural: Rolak + label_role_new: Rol berria + label_role_and_permissions: Rolak eta baimenak + label_member: Kidea + label_member_new: Kide berria + label_member_plural: Kideak + label_tracker: Aztarnaria + label_tracker_plural: Aztarnariak + label_tracker_new: Aztarnari berria + label_workflow: Lan-fluxua + label_issue_status: Zeregin egoera + label_issue_status_plural: Zeregin egoerak + label_issue_status_new: Egoera berria + label_issue_category: Zeregin kategoria + label_issue_category_plural: Zeregin kategoriak + label_issue_category_new: Kategoria berria + label_custom_field: Eremu pertsonalizatua + label_custom_field_plural: Eremu pertsonalizatuak + label_custom_field_new: Eremu pertsonalizatu berria + label_enumerations: Enumerazioak + label_enumeration_new: Balio berria + label_information: Informazioa + label_information_plural: Informazioa + label_please_login: Saioa hasi mesedez + label_register: Erregistratu + label_login_with_open_id_option: edo OpenID-rekin saioa hasi + label_password_lost: Pasahitza galduta + label_home: Hasiera + label_my_page: Nire orria + label_my_account: Nire kontua + label_my_projects: Nire proiektuak + label_administration: Kudeaketa + label_login: Saioa hasi + label_logout: Saioa bukatu + label_help: Laguntza + label_reported_issues: Berri emandako zereginak + label_assigned_to_me_issues: Niri esleitutako arazoak + label_last_login: Azken konexioa + label_registered_on: Noiz erregistratuta + label_activity: Jarduerak + label_overall_activity: Jarduera guztiak + label_user_activity: "%{value}-(r)en jarduerak" + label_new: Berria + label_logged_as: "Sartutako erabiltzailea:" + label_environment: Ingurune + label_authentication: Autentikazioa + label_auth_source: Autentikazio modua + label_auth_source_new: Autentikazio modu berria + label_auth_source_plural: Autentikazio moduak + label_subproject_plural: Azpiproiektuak + label_subproject_new: Azpiproiektu berria + label_and_its_subprojects: "%{value} eta bere azpiproiektuak" + label_min_max_length: Luzera min - max + label_list: Zerrenda + label_date: Data + label_integer: Osokoa + label_float: Koma higikorrekoa + label_boolean: Boolearra + label_string: Testua + label_text: Testu luzea + label_attribute: Atributua + label_attribute_plural: Atributuak + label_no_data: Ez dago erakusteko daturik + label_change_status: Egoera aldatu + label_history: Historikoa + label_attachment: Fitxategia + label_attachment_new: Fitxategi berria + label_attachment_delete: Fitxategia ezabatu + label_attachment_plural: Fitxategiak + label_file_added: Fitxategia gehituta + label_report: Berri ematea + label_report_plural: Berri emateak + label_news: Berria + label_news_new: Berria gehitu + label_news_plural: Berriak + label_news_latest: Azken berriak + label_news_view_all: Berri guztiak ikusi + label_news_added: Berria gehituta + label_settings: Ezarpenak + label_overview: Gainbegirada + label_version: Bertsioa + label_version_new: Bertsio berria + label_version_plural: Bertsioak + label_close_versions: Burututako bertsioak itxi + label_confirmation: Baieztapena + label_export_to: 'Eskuragarri baita:' + label_read: Irakurri... + label_public_projects: Proiektu publikoak + label_open_issues: irekita + label_open_issues_plural: irekiak + label_closed_issues: itxita + label_closed_issues_plural: itxiak + label_x_open_issues_abbr_on_total: + zero: 0 irekita / %{total} + one: 1 irekita / %{total} + other: "%{count} irekiak / %{total}" + label_x_open_issues_abbr: + zero: 0 irekita + one: 1 irekita + other: "%{count} irekiak" + label_x_closed_issues_abbr: + zero: 0 itxita + one: 1 itxita + other: "%{count} itxiak" + label_total: Guztira + label_permissions: Baimenak + label_current_status: Uneko egoera + label_new_statuses_allowed: Baimendutako egoera berriak + label_all: guztiak + label_none: ezer + label_nobody: inor + label_next: Hurrengoa + label_previous: Aurrekoak + label_used_by: Erabilita + label_details: Xehetasunak + label_add_note: Oharra gehitu + label_per_page: Orriko + label_calendar: Egutegia + label_months_from: hilabete noiztik + label_gantt: Gantt + label_internal: Barnekoa + label_last_changes: "azken %{count} aldaketak" + label_change_view_all: Aldaketa guztiak ikusi + label_personalize_page: Orri hau pertsonalizatu + label_comment: Iruzkin + label_comment_plural: Iruzkinak + label_x_comments: + zero: iruzkinik ez + one: iruzkin 1 + other: "%{count} iruzkin" + label_comment_add: Iruzkina gehitu + label_comment_added: Iruzkina gehituta + label_comment_delete: Iruzkinak ezabatu + label_query: Galdera pertsonalizatua + label_query_plural: Pertsonalizatutako galderak + label_query_new: Galdera berria + label_filter_add: Iragazkia gehitu + label_filter_plural: Iragazkiak + label_equals: da + label_not_equals: ez da + label_in_less_than: baino gutxiagotan + label_in_more_than: baino gehiagotan + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: hauetan + label_today: gaur + label_all_time: denbora guztia + label_yesterday: atzo + label_this_week: aste honetan + label_last_week: pasadan astean + label_last_n_days: "azken %{count} egunetan" + label_this_month: hilabete hau + label_last_month: pasadan hilabetea + label_this_year: urte hau + label_date_range: Data tartea + label_less_than_ago: egun hauek baino gutxiago + label_more_than_ago: egun hauek baino gehiago + label_ago: orain dela + label_contains: dauka + label_not_contains: ez dauka + label_day_plural: egun + label_repository: Biltegia + label_repository_plural: Biltegiak + label_browse: Arakatu + label_branch: Adarra + label_tag: Etiketa + label_revision: Berrikuspena + label_revision_plural: Berrikuspenak + label_revision_id: "%{value} berrikuspen" + label_associated_revisions: Elkartutako berrikuspenak + label_added: gehituta + label_modified: aldatuta + label_copied: kopiatuta + label_renamed: berrizendatuta + label_deleted: ezabatuta + label_latest_revision: Azken berrikuspena + label_latest_revision_plural: Azken berrikuspenak + label_view_revisions: Berrikuspenak ikusi + label_view_all_revisions: Berrikuspen guztiak ikusi + label_max_size: Tamaina maximoa + label_sort_highest: Goraino mugitu + label_sort_higher: Gora mugitu + label_sort_lower: Behera mugitu + label_sort_lowest: Beheraino mugitu + label_roadmap: Ibilbide-mapa + label_roadmap_due_in: "Epea: %{value}" + label_roadmap_overdue: "%{value} berandu" + label_roadmap_no_issues: Ez dago zereginik bertsio honetan + label_search: Bilatu + label_result_plural: Emaitzak + label_all_words: hitz guztiak + label_wiki: Wikia + label_wiki_edit: Wiki edizioa + label_wiki_edit_plural: Wiki edizioak + label_wiki_page: Wiki orria + label_wiki_page_plural: Wiki orriak + label_index_by_title: Izenburuaren araberako indizea + label_index_by_date: Dataren araberako indizea + label_current_version: Uneko bertsioa + label_preview: Aurreikusi + label_feed_plural: Jarioak + label_changes_details: Aldaketa guztien xehetasunak + label_issue_tracking: Zeregin jarraipena + label_spent_time: Igarotako denbora + label_f_hour: "ordu %{value}" + label_f_hour_plural: "%{value} ordu" + label_time_tracking: Denbora jarraipena + label_change_plural: Aldaketak + label_statistics: Estatistikak + label_commits_per_month: Commit-ak hilabeteka + label_commits_per_author: Commit-ak egileka + label_view_diff: Ezberdintasunak ikusi + label_diff_inline: barnean + label_diff_side_by_side: aldez alde + label_options: Aukerak + label_copy_workflow_from: Kopiatu workflow-a hemendik + label_permissions_report: Baimenen txostena + label_watched_issues: Behatutako zereginak + label_related_issues: Erlazionatutako zereginak + label_applied_status: Aplikatutako egoera + label_loading: Kargatzen... + label_relation_new: Erlazio berria + label_relation_delete: Erlazioa ezabatu + label_relates_to: erlazionatuta dago + label_duplicates: bikoizten du + label_duplicated_by: honek bikoiztuta + label_blocks: blokeatzen du + label_blocked_by: honek blokeatuta + label_precedes: aurretik doa + label_follows: jarraitzen du + label_end_to_start: bukaeratik hasierara + label_end_to_end: bukaeratik bukaerara + label_start_to_start: hasieratik hasierhasieratik bukaerara + label_start_to_end: hasieratik bukaerara + label_stay_logged_in: Saioa mantendu + label_disabled: ezgaituta + label_show_completed_versions: Bukatutako bertsioak ikusi + label_me: ni + label_board: Foroa + label_board_new: Foro berria + label_board_plural: Foroak + label_topic_plural: Gaiak + label_message_plural: Mezuak + label_message_last: Azken mezua + label_message_new: Mezu berria + label_message_posted: Mezua gehituta + label_reply_plural: Erantzunak + label_send_information: Erabiltzaileai kontuaren informazioa bidali + label_year: Urtea + label_month: Hilabetea + label_week: Astea + label_date_from: Nork + label_date_to: Nori + label_language_based: Erabiltzailearen hizkuntzaren arabera + label_sort_by: "Ordenazioa: %{value}" + label_send_test_email: Frogako mezua bidali + label_feeds_access_key: Atom atzipen giltza + label_missing_feeds_access_key: Atom atzipen giltza falta da + label_feeds_access_key_created_on: "Atom atzipen giltza orain dela %{value} sortuta" + label_module_plural: Moduluak + label_added_time_by: "%{author}, orain dela %{age} gehituta" + label_updated_time_by: "%{author}, orain dela %{age} eguneratuta" + label_updated_time: "Orain dela %{value} eguneratuta" + label_jump_to_a_project: Joan proiektura... + label_file_plural: Fitxategiak + label_changeset_plural: Aldaketak + label_default_columns: Lehenetsitako zutabeak + label_no_change_option: (Aldaketarik ez) + label_bulk_edit_selected_issues: Hautatutako zereginak batera editatu + label_theme: Itxura + label_default: Lehenetsia + label_search_titles_only: Izenburuetan bakarrik bilatu + label_user_mail_option_all: "Nire proiektu guztietako gertakari guztientzat" + label_user_mail_option_selected: "Hautatutako proiektuetako edozein gertakarientzat..." + label_user_mail_no_self_notified: "Ez dut nik egiten ditudan aldeketen jakinarazpenik jaso nahi" + label_registration_activation_by_email: kontuak epostaz gaitu + label_registration_manual_activation: kontuak eskuz gaitu + label_registration_automatic_activation: kontuak automatikoki gaitu + label_display_per_page: "Orriko: %{value}" + label_age: Adina + label_change_properties: Propietateak aldatu + label_general: Orokorra + label_more: Gehiago + label_scm: IKK + label_plugins: Pluginak + label_ldap_authentication: LDAP autentikazioa + label_downloads_abbr: Desk. + label_optional_description: Aukerako deskribapena + label_add_another_file: Beste fitxategia gehitu + label_preferences: Hobespenak + label_chronological_order: Orden kronologikoan + label_reverse_chronological_order: Alderantzizko orden kronologikoan + label_planning: Planifikazioa + label_incoming_emails: Sarrerako epostak + label_generate_key: Giltza sortu + label_issue_watchers: Behatzaileak + label_example: Adibidea + label_display: Bistaratzea + label_sort: Ordenatu + label_ascending: Gorantz + label_descending: Beherantz + label_date_from_to: "%{start}-tik %{end}-ra" + label_wiki_content_added: Wiki orria gehituta + label_wiki_content_updated: Wiki orria eguneratuta + label_group: Taldea + label_group_plural: Taldeak + label_group_new: Talde berria + label_time_entry_plural: Igarotako denbora + label_version_sharing_none: Ez partekatuta + label_version_sharing_descendants: Azpiproiektuekin + label_version_sharing_hierarchy: Proiektu Hierarkiarekin + label_version_sharing_tree: Proiektu zuhaitzarekin + label_version_sharing_system: Proiektu guztiekin + label_update_issue_done_ratios: Zereginen burututako erlazioa eguneratu + label_copy_source: Iturburua + label_copy_target: Helburua + label_copy_same_as_target: Helburuaren berdina + label_display_used_statuses_only: Aztarnari honetan erabiltzen diren egoerak bakarrik erakutsi + label_api_access_key: API atzipen giltza + label_missing_api_access_key: API atzipen giltza falta da + label_api_access_key_created_on: "API atzipen giltza sortuta orain dela %{value}" + + button_login: Saioa hasi + button_submit: Bidali + button_save: Gorde + button_check_all: Guztiak markatu + button_uncheck_all: Guztiak desmarkatu + button_delete: Ezabatu + button_create: Sortu + button_create_and_continue: Sortu eta jarraitu + button_test: Frogatu + button_edit: Editatu + button_add: Gehitu + button_change: Aldatu + button_apply: Aplikatu + button_clear: Garbitu + button_lock: Blokeatu + button_unlock: Desblokeatu + button_download: Deskargatu + button_list: Zerrenda + button_view: Ikusi + button_move: Mugitu + button_move_and_follow: Mugitu eta jarraitu + button_back: Atzera + button_cancel: Ezeztatu + button_activate: Gahitu + button_sort: Ordenatu + button_log_time: Denbora erregistratu + button_rollback: Itzuli bertsio honetara + button_watch: Behatu + button_unwatch: Behatzen utzi + button_reply: Erantzun + button_archive: Artxibatu + button_unarchive: Desartxibatu + button_reset: Berrezarri + button_rename: Berrizendatu + button_change_password: Pasahitza aldatu + button_copy: Kopiatu + button_copy_and_follow: Kopiatu eta jarraitu + button_annotate: Anotatu + button_update: Eguneratu + button_configure: Konfiguratu + button_quote: Aipatu + button_duplicate: Bikoiztu + button_show: Ikusi + + status_active: gaituta + status_registered: izena emanda + status_locked: blokeatuta + + version_status_open: irekita + version_status_locked: blokeatuta + version_status_closed: itxita + + field_active: Gaituta + + text_select_mail_notifications: Jakinarazpenak zein ekintzetarako bidaliko diren hautatu. + text_regexp_info: adib. ^[A-Z0-9]+$ + text_min_max_length_info: 0k mugarik gabe esan nahi du + text_project_destroy_confirmation: Ziur zaude proiektu hau eta erlazionatutako datu guztiak ezabatu nahi dituzula? + text_subprojects_destroy_warning: "%{value} azpiproiektuak ere ezabatuko dira." + text_workflow_edit: Hautatu rola eta aztarnaria workflow-a editatzeko + text_are_you_sure: Ziur zaude? + text_journal_changed: "%{label} %{old}-(e)tik %{new}-(e)ra aldatuta" + text_journal_set_to: "%{label}-k %{value} balioa hartu du" + text_journal_deleted: "%{label} ezabatuta (%{old})" + text_journal_added: "%{label} %{value} gehituta" + text_tip_issue_begin_day: gaur hasten diren zereginak + text_tip_issue_end_day: gaur bukatzen diren zereginak + text_tip_issue_begin_end_day: gaur hasi eta bukatzen diren zereginak + text_caracters_maximum: "%{count} karaktere gehienez." + text_caracters_minimum: "Gutxienez %{count} karaktereetako luzerakoa izan behar du." + text_length_between: "Luzera %{min} eta %{max} karaktereen artekoa." + text_tracker_no_workflow: Ez da workflow-rik definitu aztarnari honentzako + text_unallowed_characters: Debekatutako karaktereak + text_comma_separated: Balio anitz izan daitezke (komaz banatuta). + text_line_separated: Balio anitz izan daitezke (balio bakoitza lerro batean). + text_issues_ref_in_commit_messages: Commit-en mezuetan zereginak erlazionatu eta konpontzen + text_issue_added: "%{id} zeregina %{author}-(e)k jakinarazi du." + text_issue_updated: "%{id} zeregina %{author}-(e)k eguneratu du." + text_wiki_destroy_confirmation: Ziur zaude wiki hau eta bere eduki guztiak ezabatu nahi dituzula? + text_issue_category_destroy_question: "Zeregin batzuk (%{count}) kategoria honetara esleituta daude. Zer egin nahi duzu?" + text_issue_category_destroy_assignments: Kategoria esleipenak kendu + text_issue_category_reassign_to: Zereginak kategoria honetara esleitu + text_user_mail_option: "Hautatu gabeko proiektuetan, behatzen edo parte hartzen duzun gauzei buruzko jakinarazpenak jasoko dituzu (adib. zu egile zaren edo esleituta dituzun zereginak)." + text_no_configuration_data: "Rolak, aztarnariak, zeregin egoerak eta workflow-ak ez dira oraindik konfiguratu.\nOso gomendagarria de lehenetsitako kkonfigurazioa kargatzea. Kargatu eta gero aldatu ahalko duzu." + text_load_default_configuration: Lehenetsitako konfigurazioa kargatu + text_status_changed_by_changeset: "%{value} aldaketan aplikatuta." + text_issues_destroy_confirmation: 'Ziur zaude hautatutako zeregina(k) ezabatu nahi dituzula?' + text_select_project_modules: 'Hautatu proiektu honetan gaitu behar diren moduluak:' + text_default_administrator_account_changed: Lehenetsitako kudeatzaile kontua aldatuta + text_file_repository_writable: Eranskinen direktorioan idatz daiteke + text_plugin_assets_writable: Pluginen baliabideen direktorioan idatz daiteke + text_rmagick_available: RMagick eskuragarri (aukerazkoa) + text_destroy_time_entries_question: "%{hours} orduei buruz berri eman zen zuk ezabatzera zoazen zereginean. Zer egin nahi duzu?" + text_destroy_time_entries: Ezabatu berri emandako orduak + text_assign_time_entries_to_project: Berri emandako orduak proiektura esleitu + text_reassign_time_entries: 'Berri emandako orduak zeregin honetara esleitu:' + text_user_wrote: "%{value}-(e)k idatzi zuen:" + text_enumeration_destroy_question: "%{count} objetu balio honetara esleituta daude." + text_enumeration_category_reassign_to: 'Beste balio honetara esleitu:' + text_email_delivery_not_configured: "Eposta bidalketa ez dago konfiguratuta eta jakinarazpenak ezgaituta daude.\nKonfiguratu zure SMTP zerbitzaria config/configuration.yml-n eta aplikazioa berrabiarazi hauek gaitzeko." + text_repository_usernames_mapping: "Hautatu edo eguneratu Redmineko erabiltzailea biltegiko egunkarietan topatzen diren erabiltzaile izenekin erlazionatzeko.\nRedmine-n eta biltegian erabiltzaile izen edo eposta berdina duten erabiltzaileak automatikoki erlazionatzen dira." + text_diff_truncated: '... Diff hau moztua izan da erakus daitekeen tamaina maximoa gainditu duelako.' + text_custom_field_possible_values_info: 'Lerro bat balio bakoitzeko' + text_wiki_page_destroy_question: "Orri honek %{descendants} orri seme eta ondorengo ditu. Zer egin nahi duzu?" + text_wiki_page_nullify_children: "Orri semeak erro orri moduan mantendu" + text_wiki_page_destroy_children: "Orri semeak eta beraien ondorengo guztiak ezabatu" + text_wiki_page_reassign_children: "Orri semeak orri guraso honetara esleitu" + text_own_membership_delete_confirmation: "Zure baimen batzuk (edo guztiak) kentzera zoaz eta baliteke horren ondoren proiektu hau ezin editatzea.\n Ziur zaude jarraitu nahi duzula?" + + default_role_manager: Kudeatzailea + default_role_developer: Garatzailea + default_role_reporter: Berriemailea + default_tracker_bug: Errorea + default_tracker_feature: Eginbidea + default_tracker_support: Laguntza + default_issue_status_new: Berria + default_issue_status_in_progress: Lanean + default_issue_status_resolved: Ebatzita + default_issue_status_feedback: Berrelikadura + default_issue_status_closed: Itxita + default_issue_status_rejected: Baztertua + default_doc_category_user: Erabiltzaile dokumentazioa + default_doc_category_tech: Dokumentazio teknikoa + default_priority_low: Baxua + default_priority_normal: Normala + default_priority_high: Altua + default_priority_urgent: Larria + default_priority_immediate: Berehalakoa + default_activity_design: Diseinua + default_activity_development: Garapena + + enumeration_issue_priorities: Zeregin lehentasunak + enumeration_doc_categories: Dokumentu kategoriak + enumeration_activities: Jarduerak (denbora kontrola)) + enumeration_system_activity: Sistemako Jarduera + label_board_sticky: Itsaskorra + label_board_locked: Blokeatuta + permission_export_wiki_pages: Wiki orriak esportatu + setting_cache_formatted_text: Formatudun testua katxeatu + permission_manage_project_activities: Proiektuaren jarduerak kudeatu + error_unable_delete_issue_status: Ezine da zereginaren egoera ezabatu + label_profile: Profila + permission_manage_subtasks: Azpiatazak kudeatu + field_parent_issue: Zeregin gurasoa + label_subtask_plural: Azpiatazak + label_project_copy_notifications: Proiektua kopiatzen den bitartean eposta jakinarazpenak bidali + error_can_not_delete_custom_field: Ezin da eremu pertsonalizatua ezabatu + error_unable_to_connect: Ezin da konektatu (%{value}) + error_can_not_remove_role: Rol hau erabiltzen hari da eta ezin da ezabatu. + error_can_not_delete_tracker: Aztarnari honek zereginak ditu eta ezin da ezabatu. + field_principal: Ekintzaile + label_my_page_block: "Nire orriko blokea" + notice_failed_to_save_members: "Kidea(k) gordetzean errorea: %{errors}." + text_zoom_out: Zooma txikiagotu + text_zoom_in: Zooma handiagotu + notice_unable_delete_time_entry: "Ezin da hautatutako denbora erregistroa ezabatu." + label_overall_spent_time: Igarotako denbora guztira + field_time_entries: "Denbora erregistratu" + project_module_gantt: Gantt + project_module_calendar: Egutegia + button_edit_associated_wikipage: "Esleitutako wiki orria editatu: %{page_title}" + field_text: Testu eremua + label_user_mail_option_only_owner: "Jabea naizen gauzetarako barrarik" + setting_default_notification_option: "Lehenetsitako ohartarazpen aukera" + label_user_mail_option_only_my_events: "Behatzen ditudan edo partaide naizen gauzetarako bakarrik" + label_user_mail_option_only_assigned: "Niri esleitutako gauzentzat bakarrik" + label_user_mail_option_none: "Gertakaririk ez" + field_member_of_group: "Esleituta duenaren taldea" + field_assigned_to_role: "Esleituta duenaren rola" + notice_not_authorized_archived_project: "Atzitu nahi duzun proiektua artxibatua izan da." + label_principal_search: "Bilatu erabiltzaile edo taldea:" + label_user_search: "Erabiltzailea bilatu:" + field_visible: Ikusgai + setting_emails_header: "Eposten goiburua" + setting_commit_logtime_activity_id: "Erregistratutako denboraren jarduera" + text_time_logged_by_changeset: "%{value} aldaketan egindakoa." + setting_commit_logtime_enabled: "Erregistrutako denbora gaitu" + notice_gantt_chart_truncated: Grafikoa moztu da bistara daitekeen elementuen kopuru maximoa gainditu delako (%{max}) + setting_gantt_items_limit: "Gantt grafikoan bistara daitekeen elementu kopuru maximoa" + field_warn_on_leaving_unsaved: Gorde gabeko testua duen orri batetik ateratzen naizenean ohartarazi + text_warn_on_leaving_unsaved: Uneko orritik joaten bazara gorde gabeko testua galduko da. + label_my_queries: Nire galdera pertsonalizatuak + text_journal_changed_no_detail: "%{label} eguneratuta" + label_news_comment_added: Berri batera iruzkina gehituta + button_expand_all: Guztia zabaldu + button_collapse_all: Guztia tolestu + label_additional_workflow_transitions_for_assignee: Erabiltzaileak esleitua duenean baimendutako transtsizio gehigarriak + label_additional_workflow_transitions_for_author: Erabiltzailea egilea denean baimendutako transtsizio gehigarriak + label_bulk_edit_selected_time_entries: Hautatutako denbora egunkariak batera editatu + text_time_entries_destroy_confirmation: Ziur zaude hautatutako denbora egunkariak ezabatu nahi dituzula? + label_role_anonymous: Ezezaguna + label_role_non_member: Ez kidea + label_issue_note_added: Oharra gehituta + label_issue_status_updated: Egoera eguneratuta + label_issue_priority_updated: Lehentasuna eguneratuta + label_issues_visibility_own: Erabiltzaileak sortu edo esleituta dituen zereginak + field_issues_visibility: Zeregin ikusgarritasuna + label_issues_visibility_all: Zeregin guztiak + permission_set_own_issues_private: Nork bere zereginak publiko edo pribatu jarri + field_is_private: Pribatu + permission_set_issues_private: Zereginak publiko edo pribatu jarri + label_issues_visibility_public: Pribatu ez diren zeregin guztiak + text_issues_destroy_descendants_confirmation: Honek %{count} azpiataza ezabatuko ditu baita ere. + field_commit_logs_encoding: Commit-en egunkarien kodetzea + field_scm_path_encoding: Bidearen kodeketa + text_scm_path_encoding_note: "Lehentsita: UTF-8" + field_path_to_repository: Biltegirako bidea + field_root_directory: Erro direktorioa + field_cvs_module: Modulua + field_cvsroot: CVSROOT + text_mercurial_repository_note: Biltegi locala (adib. /hgrepo, c:\hgrepo) + text_scm_command: Komandoa + text_scm_command_version: Bertsioa + 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 zeregina + one: 1 zeregina + other: "%{count} zereginak" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: guztiak + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Azpiproiektuekin + label_cross_project_tree: Proiektu zuhaitzarekin + label_cross_project_hierarchy: Proiektu Hierarkiarekin + label_cross_project_system: Proiektu guztiekin + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Guztira + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a8c919f646bcf8d04d61e354ffe1fb44ea0d6e4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a8c919f646bcf8d04d61e354ffe1fb44ea0d6e4.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,41 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 BoardsHelper + def board_breadcrumb(item) + board = item.is_a?(Message) ? item.board : item + links = [link_to(l(:label_board_plural), project_boards_path(item.project))] + boards = board.ancestors.reverse + if item.is_a?(Message) + boards << board + end + links += boards.map {|ancestor| link_to(h(ancestor.name), project_board_path(ancestor.project, ancestor))} + breadcrumb links + end + + def boards_options_for_select(boards) + options = [] + Board.board_tree(boards) do |board, level| + label = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe + label << board.name + options << [label, board.id] + end + options + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a9c163fd859224f6bc9ce5b56705d82e3f87035.svn-base --- a/.svn/pristine/1a/1a9c163fd859224f6bc9ce5b56705d82e3f87035.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -

    <%= link_to l(@enumeration.option_name), enumerations_path %> » <%=h @enumeration %>

    - -<%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %> - <%= render :partial => 'form', :locals => {:f => f} %> - <%= submit_tag l(:button_save) %> -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a9d1a49e8d55feb048c04fbca5352f495ad3bfd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a9d1a49e8d55feb048c04fbca5352f495ad3bfd.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,43 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::CustomFieldsTest < Redmine::ApiTest::Base + fixtures :users, :custom_fields + + def setup + Setting.rest_api_enabled = '1' + end + + test "GET /custom_fields.xml should return custom fields" do + get '/custom_fields.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'custom_fields' do + assert_select 'custom_field' do + assert_select 'name', :text => 'Database' + assert_select 'id', :text => '2' + assert_select 'customized_type', :text => 'issue' + assert_select 'possible_values[type=array]' do + assert_select 'possible_value>value', :text => 'PostgreSQL' + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1a9dc03dd504c9d145f93f49fdb8e42950ff1c74.svn-base --- a/.svn/pristine/1a/1a9dc03dd504c9d145f93f49fdb8e42950ff1c74.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingIssueRelationsTest < ActionController::IntegrationTest - def test_issue_relations - assert_routing( - { :method => 'get', :path => "/issues/1/relations" }, - { :controller => 'issue_relations', :action => 'index', - :issue_id => '1' } - ) - assert_routing( - { :method => 'get', :path => "/issues/1/relations.xml" }, - { :controller => 'issue_relations', :action => 'index', - :issue_id => '1', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/issues/1/relations.json" }, - { :controller => 'issue_relations', :action => 'index', - :issue_id => '1', :format => 'json' } - ) - assert_routing( - { :method => 'post', :path => "/issues/1/relations" }, - { :controller => 'issue_relations', :action => 'create', - :issue_id => '1' } - ) - assert_routing( - { :method => 'post', :path => "/issues/1/relations.xml" }, - { :controller => 'issue_relations', :action => 'create', - :issue_id => '1', :format => 'xml' } - ) - assert_routing( - { :method => 'post', :path => "/issues/1/relations.json" }, - { :controller => 'issue_relations', :action => 'create', - :issue_id => '1', :format => 'json' } - ) - assert_routing( - { :method => 'get', :path => "/relations/23" }, - { :controller => 'issue_relations', :action => 'show', :id => '23' } - ) - assert_routing( - { :method => 'get', :path => "/relations/23.xml" }, - { :controller => 'issue_relations', :action => 'show', :id => '23', - :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/relations/23.json" }, - { :controller => 'issue_relations', :action => 'show', :id => '23', - :format => 'json' } - ) - assert_routing( - { :method => 'delete', :path => "/relations/23" }, - { :controller => 'issue_relations', :action => 'destroy', :id => '23' } - ) - assert_routing( - { :method => 'delete', :path => "/relations/23.xml" }, - { :controller => 'issue_relations', :action => 'destroy', :id => '23', - :format => 'xml' } - ) - assert_routing( - { :method => 'delete', :path => "/relations/23.json" }, - { :controller => 'issue_relations', :action => 'destroy', :id => '23', - :format => 'json' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1ab06441767b89700eccb7f62767523259a141ff.svn-base --- a/.svn/pristine/1a/1ab06441767b89700eccb7f62767523259a141ff.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -class Company < ActiveRecord::Base - attr_protected :rating - set_sequence_name :companies_nonstd_seq - - validates_presence_of :name - def validate - errors.add('rating', 'rating should not be 2') if rating == 2 - end -end \ No newline at end of file diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1ad76dc367c4343626856549804aea889d0fbd2d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ad76dc367c4343626856549804aea889d0fbd2d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 DocumentTest < ActiveSupport::TestCase + fixtures :projects, :enumerations, :documents, :attachments, + :enabled_modules, + :users, :members, :member_roles, :roles, + :groups_users + + def test_create + doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation')) + assert doc.save + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(document_added) do + doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation')) + assert doc.save + end + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_with_default_category + # Sets a default category + e = Enumeration.find_by_name('Technical documentation') + e.update_attributes(:is_default => true) + + doc = Document.new(:project => Project.find(1), :title => 'New document') + assert_equal e, doc.category + assert doc.save + end + + def test_updated_on_with_attachments + d = Document.find(1) + assert d.attachments.any? + assert_equal d.attachments.map(&:created_on).max, d.updated_on + end + + def test_updated_on_without_attachments + d = Document.find(2) + assert d.attachments.empty? + assert_equal d.created_on, d.updated_on + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1ae20ea87464b5769eed93f455558d2b0e19460e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ae20ea87464b5769eed93f455558d2b0e19460e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,24 @@ +class AddUniqueIndexOnCustomFieldsProjects < ActiveRecord::Migration + def up + table_name = "#{CustomField.table_name_prefix}custom_fields_projects#{CustomField.table_name_suffix}" + duplicates = CustomField.connection.select_rows("SELECT custom_field_id, project_id FROM #{table_name} GROUP BY custom_field_id, project_id HAVING COUNT(*) > 1") + duplicates.each do |custom_field_id, project_id| + # Removes duplicate rows + CustomField.connection.execute("DELETE FROM #{table_name} WHERE custom_field_id=#{custom_field_id} AND project_id=#{project_id}") + # And insert one + CustomField.connection.execute("INSERT INTO #{table_name} (custom_field_id, project_id) VALUES (#{custom_field_id}, #{project_id})") + end + + if index_exists? :custom_fields_projects, [:custom_field_id, :project_id] + remove_index :custom_fields_projects, [:custom_field_id, :project_id] + end + add_index :custom_fields_projects, [:custom_field_id, :project_id], :unique => true + end + + def down + if index_exists? :custom_fields_projects, [:custom_field_id, :project_id] + remove_index :custom_fields_projects, [:custom_field_id, :project_id] + end + add_index :custom_fields_projects, [:custom_field_id, :project_id] + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1ae8ac309393cfb207f1a6763cafea128ad11893.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ae8ac309393cfb207f1a6763cafea128ad11893.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1107 @@ +# Indonesian translations +# by Raden Prabowo (cakbowo@gmail.com) + +id: + direction: ltr + date: + formats: + default: "%d-%m-%Y" + short: "%d %b" + long: "%d %B %Y" + + day_names: [Minggu, Senin, Selasa, Rabu, Kamis, Jumat, Sabtu] + abbr_day_names: [Ming, Sen, Sel, Rab, Kam, Jum, Sab] + + month_names: [~, Januari, Februari, Maret, April, Mei, Juni, Juli, Agustus, September, Oktober, November, Desember] + abbr_month_names: [~, Jan, Feb, Mar, Apr, Mei, Jun, Jul, Agu, Sep, Okt, Nov, Des] + order: + - :day + - :month + - :year + + time: + formats: + default: "%a %d %b %Y, %H:%M:%S" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%d %B %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "setengah menit" + less_than_x_seconds: + one: "kurang dari sedetik" + other: "kurang dari %{count} detik" + x_seconds: + one: "sedetik" + other: "%{count} detik" + less_than_x_minutes: + one: "kurang dari semenit" + other: "kurang dari %{count} menit" + x_minutes: + one: "semenit" + other: "%{count} menit" + about_x_hours: + one: "sekitar sejam" + other: "sekitar %{count} jam" + x_hours: + one: "1 jam" + other: "%{count} jam" + x_days: + one: "sehari" + other: "%{count} hari" + about_x_months: + one: "sekitar sebulan" + other: "sekitar %{count} bulan" + x_months: + one: "sebulan" + other: "%{count} bulan" + about_x_years: + one: "sekitar setahun" + other: "sekitar %{count} tahun" + over_x_years: + one: "lebih dari setahun" + other: "lebih dari %{count} tahun" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + format: + precision: 3 + separator: ',' + delimiter: '.' + currency: + format: + unit: 'Rp' + precision: 2 + format: '%n %u' + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: + sentence_connector: "dan" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "tidak termasuk dalam daftar" + exclusion: "sudah dicadangkan" + invalid: "salah" + confirmation: "tidak sesuai konfirmasi" + accepted: "harus disetujui" + empty: "tidak boleh kosong" + blank: "tidak boleh kosong" + too_long: "terlalu panjang (maksimum %{count} karakter)" + too_short: "terlalu pendek (minimum %{count} karakter)" + wrong_length: "panjangnya salah (seharusnya %{count} karakter)" + taken: "sudah diambil" + not_a_number: "bukan angka" + not_a_date: "bukan tanggal" + greater_than: "harus lebih besar dari %{count}" + greater_than_or_equal_to: "harus lebih besar atau sama dengan %{count}" + equal_to: "harus sama dengan %{count}" + less_than: "harus kurang dari %{count}" + less_than_or_equal_to: "harus kurang atau sama dengan %{count}" + odd: "harus ganjil" + even: "harus genap" + greater_than_start_date: "harus lebih besar dari tanggal mulai" + not_same_project: "tidak tergabung dalam proyek yang sama" + circular_dependency: "kaitan ini akan menghasilkan circular dependency" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Silakan pilih + + general_text_No: 'Tidak' + general_text_Yes: 'Ya' + general_text_no: 'tidak' + general_text_yes: 'ya' + general_lang_name: 'Indonesia' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-1 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_updated: Akun sudah berhasil diperbarui. + notice_account_invalid_creditentials: Pengguna atau kata sandi salah + notice_account_password_updated: Kata sandi sudah berhasil diperbarui. + notice_account_wrong_password: Kata sandi salah. + notice_account_register_done: Akun sudah berhasil dibuat. Untuk mengaktifkan akun anda, silakan klik tautan (link) yang dikirim kepada anda melalui e-mail. + notice_account_unknown_email: Pengguna tidak dikenal. + notice_can_t_change_password: Akun ini menggunakan sumber otentikasi eksternal yang tidak dikenal. Kata sandi tidak bisa diubah. + notice_account_lost_email_sent: Email berisi instruksi untuk memilih kata sandi baru sudah dikirimkan kepada anda. + notice_account_activated: Akun anda sudah diaktifasi. Sekarang anda bisa login. + notice_successful_create: Berhasil dibuat. + notice_successful_update: Berhasil diperbarui. + notice_successful_delete: Berhasil dihapus. + notice_successful_connection: Berhasil terhubung. + notice_file_not_found: Berkas yang anda buka tidak ada atau sudah dihapus. + notice_locking_conflict: Data sudah diubah oleh pengguna lain. + notice_not_authorized: Anda tidak memiliki akses ke halaman ini. + notice_email_sent: "Email sudah dikirim ke %{value}" + notice_email_error: "Terjadi kesalahan pada saat pengiriman email (%{value})" + notice_feeds_access_key_reseted: Atom access key anda sudah direset. + notice_failed_to_save_issues: "Gagal menyimpan %{count} masalah dari %{total} yang dipilih: %{ids}." + notice_no_issue_selected: "Tidak ada masalah yang dipilih! Silakan pilih masalah yang akan anda sunting." + notice_account_pending: "Akun anda sudah dibuat dan sekarang sedang menunggu persetujuan administrator." + notice_default_data_loaded: Konfigurasi default sudah berhasil dimuat. + notice_unable_delete_version: Tidak bisa menghapus versi. + + error_can_t_load_default_data: "Konfigurasi default tidak bisa dimuat: %{value}" + error_scm_not_found: "Entri atau revisi tidak terdapat pada repositori." + error_scm_command_failed: "Terjadi kesalahan pada saat mengakses repositori: %{value}" + error_scm_annotate: "Entri tidak ada, atau tidak dapat di anotasi." + error_issue_not_found_in_project: 'Masalah tidak ada atau tidak tergabung dalam proyek ini.' + error_no_tracker_in_project: 'Tidak ada pelacak yang diasosiasikan pada proyek ini. Silakan pilih Pengaturan Proyek.' + error_no_default_issue_status: 'Nilai default untuk Status masalah belum didefinisikan. Periksa kembali konfigurasi anda (Pilih "Administrasi --> Status masalah").' + error_can_not_reopen_issue_on_closed_version: 'Masalah yang ditujukan pada versi tertutup tidak bisa dibuka kembali' + error_can_not_archive_project: Proyek ini tidak bisa diarsipkan + + warning_attachments_not_saved: "%{count} berkas tidak bisa disimpan." + + mail_subject_lost_password: "Kata sandi %{value} anda" + mail_body_lost_password: 'Untuk mengubah kata sandi anda, klik tautan berikut::' + mail_subject_register: "Aktivasi akun %{value} anda" + mail_body_register: 'Untuk mengaktifkan akun anda, klik tautan berikut:' + mail_body_account_information_external: "Anda dapat menggunakan akun %{value} anda untuk login." + mail_body_account_information: Informasi akun anda + mail_subject_account_activation_request: "Permintaan aktivasi akun %{value} " + mail_body_account_activation_request: "Pengguna baru (%{value}) sudan didaftarkan. Akun tersebut menunggu persetujuan anda:" + mail_subject_reminder: "%{count} masalah harus selesai pada hari berikutnya (%{days})" + mail_body_reminder: "%{count} masalah yang ditugaskan pada anda harus selesai dalam %{days} hari kedepan:" + mail_subject_wiki_content_added: "'%{id}' halaman wiki sudah ditambahkan" + mail_body_wiki_content_added: "The '%{id}' halaman wiki sudah ditambahkan oleh %{author}." + mail_subject_wiki_content_updated: "'%{id}' halaman wiki sudah diperbarui" + mail_body_wiki_content_updated: "The '%{id}' halaman wiki sudah diperbarui oleh %{author}." + + + field_name: Nama + field_description: Deskripsi + field_summary: Ringkasan + field_is_required: Dibutuhkan + field_firstname: Nama depan + field_lastname: Nama belakang + field_mail: Email + field_filename: Berkas + field_filesize: Ukuran + field_downloads: Unduhan + field_author: Pengarang + field_created_on: Dibuat + field_updated_on: Diperbarui + field_field_format: Format + field_is_for_all: Untuk semua proyek + field_possible_values: Nilai yang mungkin + field_regexp: Regular expression + field_min_length: Panjang minimum + field_max_length: Panjang maksimum + field_value: Nilai + field_category: Kategori + field_title: Judul + field_project: Proyek + field_issue: Masalah + field_status: Status + field_notes: Catatan + field_is_closed: Masalah ditutup + field_is_default: Nilai default + field_tracker: Pelacak + field_subject: Perihal + field_due_date: Harus selesai + field_assigned_to: Ditugaskan ke + field_priority: Prioritas + field_fixed_version: Versi target + field_user: Pengguna + field_role: Peran + field_homepage: Halaman web + field_is_public: Publik + field_parent: Subproyek dari + field_is_in_roadmap: Masalah ditampilkan di rencana kerja + field_login: Login + field_mail_notification: Notifikasi email + field_admin: Administrator + field_last_login_on: Terakhir login + field_language: Bahasa + field_effective_date: Tanggal + field_password: Kata sandi + field_new_password: Kata sandi baru + field_password_confirmation: Konfirmasi + field_version: Versi + field_type: Tipe + field_host: Host + field_port: Port + field_account: Akun + field_base_dn: Base DN + field_attr_login: Atribut login + field_attr_firstname: Atribut nama depan + field_attr_lastname: Atribut nama belakang + field_attr_mail: Atribut email + field_onthefly: Pembuatan pengguna seketika + field_start_date: Mulai + field_done_ratio: "% Selesai" + field_auth_source: Mode otentikasi + field_hide_mail: Sembunyikan email saya + field_comments: Komentar + field_url: URL + field_start_page: Halaman awal + field_subproject: Subproyek + field_hours: Jam + field_activity: Kegiatan + field_spent_on: Tanggal + field_identifier: Pengenal + field_is_filter: Digunakan sebagai penyaring + field_issue_to: Masalah terkait + field_delay: Tertunday + field_assignable: Masalah dapat ditugaskan pada peran ini + field_redirect_existing_links: Alihkan tautan yang ada + field_estimated_hours: Perkiraan waktu + field_column_names: Kolom + field_time_zone: Zona waktu + field_searchable: Dapat dicari + field_default_value: Nilai default + field_comments_sorting: Tampilkan komentar + field_parent_title: Halaman induk + field_editable: Dapat disunting + field_watcher: Pemantau + field_identity_url: OpenID URL + field_content: Isi + field_group_by: Dikelompokkan berdasar + field_sharing: Berbagi + + setting_app_title: Judul aplikasi + setting_app_subtitle: Subjudul aplikasi + setting_welcome_text: Teks sambutan + setting_default_language: Bahasa Default + setting_login_required: Butuhkan otentikasi + setting_self_registration: Swa-pendaftaran + setting_attachment_max_size: Ukuran maksimum untuk lampiran + setting_issues_export_limit: Batasan ukuran export masalah + setting_mail_from: Emisi alamat email + setting_bcc_recipients: Blind carbon copy recipients (bcc) + setting_plain_text_mail: Plain text mail (no HTML) + setting_host_name: Nama host dan path + setting_text_formatting: Format teks + setting_wiki_compression: Kompresi untuk riwayat wiki + setting_feeds_limit: Batasan isi feed + setting_default_projects_public: Proyek baru defaultnya adalah publik + setting_autofetch_changesets: Autofetch commits + setting_sys_api_enabled: Aktifkan WS untuk pengaturan repositori + setting_commit_ref_keywords: Referensi kaca kunci + setting_commit_fix_keywords: Pembetulan kaca kunci + setting_autologin: Autologin + setting_date_format: Format tanggal + setting_time_format: Format waktu + setting_cross_project_issue_relations: Perbolehkan kaitan masalah proyek berbeda + setting_issue_list_default_columns: Kolom default ditampilkan di daftar masalah + setting_emails_footer: Footer untuk email + setting_protocol: Protokol + setting_per_page_options: Pilihan obyek per halaman + setting_user_format: Format tampilan untuk pengguna + setting_activity_days_default: Hari tertampil pada kegiatan proyek + setting_display_subprojects_issues: Secara default, tampilkan masalah subproyek pada proyek utama + setting_enabled_scm: Enabled SCM + setting_mail_handler_api_enabled: Enable WS for incoming emails + setting_mail_handler_api_key: API key + setting_sequential_project_identifiers: Buat pengenal proyek terurut + setting_gravatar_enabled: Gunakan icon pengguna dari Gravatar + setting_gravatar_default: Gambar default untuk Gravatar + setting_diff_max_lines_displayed: Maksimum perbedaan baris tertampil + setting_file_max_size_displayed: Maksimum berkas tertampil secara inline + setting_repository_log_display_limit: Nilai maksimum dari revisi ditampilkan di log berkas + setting_openid: Perbolehkan Login dan pendaftaran melalui OpenID + setting_password_min_length: Panjang minimum untuk kata sandi + setting_new_project_user_role_id: Peran diberikan pada pengguna non-admin yang membuat proyek + setting_default_projects_modules: Modul yang diaktifkan pada proyek baru + + permission_add_project: Tambahkan proyek + permission_edit_project: Sunting proyek + permission_select_project_modules: Pilih modul proyek + permission_manage_members: Atur anggota + permission_manage_versions: Atur versi + permission_manage_categories: Atur kategori masalah + permission_add_issues: Tambahkan masalah + permission_edit_issues: Sunting masalah + permission_manage_issue_relations: Atur kaitan masalah + permission_add_issue_notes: Tambahkan catatan + permission_edit_issue_notes: Sunting catatan + permission_edit_own_issue_notes: Sunting catatan saya + permission_move_issues: Pindahkan masalah + permission_delete_issues: Hapus masalah + permission_manage_public_queries: Atur query publik + permission_save_queries: Simpan query + permission_view_gantt: Tampilkan gantt chart + permission_view_calendar: Tampilkan kalender + permission_view_issue_watchers: Tampilkan daftar pemantau + permission_add_issue_watchers: Tambahkan pemantau + permission_delete_issue_watchers: Hapus pemantau + permission_log_time: Log waktu terpakai + permission_view_time_entries: Tampilkan waktu terpakai + permission_edit_time_entries: Sunting catatan waktu + permission_edit_own_time_entries: Sunting catatan waktu saya + permission_manage_news: Atur berita + permission_comment_news: Komentari berita + permission_view_documents: Tampilkan dokumen + permission_manage_files: Atur berkas + permission_view_files: Tampilkan berkas + permission_manage_wiki: Atur wiki + permission_rename_wiki_pages: Ganti nama halaman wiki + permission_delete_wiki_pages: Hapus halaman wiki + permission_view_wiki_pages: Tampilkan wiki + permission_view_wiki_edits: Tampilkan riwayat wiki + permission_edit_wiki_pages: Sunting halaman wiki + permission_delete_wiki_pages_attachments: Hapus lampiran + permission_protect_wiki_pages: Proteksi halaman wiki + permission_manage_repository: Atur repositori + permission_browse_repository: Jelajah repositori + permission_view_changesets: Tampilkan set perubahan + permission_commit_access: Commit akses + permission_manage_boards: Atur forum + permission_view_messages: Tampilkan pesan + permission_add_messages: Tambahkan pesan + permission_edit_messages: Sunting pesan + permission_edit_own_messages: Sunting pesan saya + permission_delete_messages: Hapus pesan + permission_delete_own_messages: Hapus pesan saya + + project_module_issue_tracking: Pelacak masalah + project_module_time_tracking: Pelacak waktu + project_module_news: Berita + project_module_documents: Dokumen + project_module_files: Berkas + project_module_wiki: Wiki + project_module_repository: Repositori + project_module_boards: Forum + + label_user: Pengguna + label_user_plural: Pengguna + label_user_new: Pengguna baru + label_user_anonymous: Anonymous + label_project: Proyek + label_project_new: Proyek baru + label_project_plural: Proyek + label_x_projects: + zero: tidak ada proyek + one: 1 proyek + other: "%{count} proyek" + label_project_all: Semua Proyek + label_project_latest: Proyek terakhir + label_issue: Masalah + label_issue_new: Masalah baru + label_issue_plural: Masalah + label_issue_view_all: tampilkan semua masalah + label_issues_by: "Masalah ditambahkan oleh %{value}" + label_issue_added: Masalah ditambahan + label_issue_updated: Masalah diperbarui + label_document: Dokumen + label_document_new: Dokumen baru + label_document_plural: Dokumen + label_document_added: Dokumen ditambahkan + label_role: Peran + label_role_plural: Peran + label_role_new: Peran baru + label_role_and_permissions: Peran dan perijinan + label_member: Anggota + label_member_new: Anggota baru + label_member_plural: Anggota + label_tracker: Pelacak + label_tracker_plural: Pelacak + label_tracker_new: Pelacak baru + label_workflow: Alur kerja + label_issue_status: Status masalah + label_issue_status_plural: Status masalah + label_issue_status_new: Status baru + label_issue_category: Kategori masalah + label_issue_category_plural: Kategori masalah + label_issue_category_new: Kategori baru + label_custom_field: Field kustom + label_custom_field_plural: Field kustom + label_custom_field_new: Field kustom + label_enumerations: Enumerasi + label_enumeration_new: Buat baru + label_information: Informasi + label_information_plural: Informasi + label_please_login: Silakan login + label_register: mendaftar + label_login_with_open_id_option: atau login menggunakan OpenID + label_password_lost: Lupa password + label_home: Halaman depan + label_my_page: Beranda + label_my_account: Akun saya + label_my_projects: Proyek saya + label_administration: Administrasi + label_login: Login + label_logout: Keluar + label_help: Bantuan + label_reported_issues: Masalah terlapor + label_assigned_to_me_issues: Masalah yang ditugaskan pada saya + label_last_login: Terakhir login + label_registered_on: Terdaftar pada + label_activity: Kegiatan + label_overall_activity: Kegiatan umum + label_user_activity: "kegiatan %{value}" + label_new: Baru + label_logged_as: Login sebagai + label_environment: Lingkungan + label_authentication: Otentikasi + label_auth_source: Mode Otentikasi + label_auth_source_new: Mode otentikasi baru + label_auth_source_plural: Mode Otentikasi + label_subproject_plural: Subproyek + label_and_its_subprojects: "%{value} dan subproyeknya" + label_min_max_length: Panjang Min - Maks + label_list: Daftar + label_date: Tanggal + label_integer: Integer + label_float: Float + label_boolean: Boolean + label_string: Text + label_text: Long text + label_attribute: Atribut + label_attribute_plural: Atribut + label_no_data: Tidak ada data untuk ditampilkan + label_change_status: Status perubahan + label_history: Riwayat + label_attachment: Berkas + label_attachment_new: Berkas baru + label_attachment_delete: Hapus Berkas + label_attachment_plural: Berkas + label_file_added: Berkas ditambahkan + label_report: Laporan + label_report_plural: Laporan + label_news: Berita + label_news_new: Tambahkan berita + label_news_plural: Berita + label_news_latest: Berita terakhir + label_news_view_all: Tampilkan semua berita + label_news_added: Berita ditambahkan + label_settings: Pengaturan + label_overview: Umum + label_version: Versi + label_version_new: Versi baru + label_version_plural: Versi + label_confirmation: Konfirmasi + label_export_to: 'Juga tersedia dalam:' + label_read: Baca... + label_public_projects: Proyek publik + label_open_issues: belum selesai + label_open_issues_plural: belum selesai + label_closed_issues: selesai + label_closed_issues_plural: selesai + label_x_open_issues_abbr_on_total: + zero: 0 belum selesai / %{total} + one: 1 belum selesai / %{total} + other: "%{count} terbuka / %{total}" + label_x_open_issues_abbr: + zero: 0 belum selesai + one: 1 belum selesai + other: "%{count} belum selesai" + label_x_closed_issues_abbr: + zero: 0 selesai + one: 1 selesai + other: "%{count} selesai" + label_total: Total + label_permissions: Perijinan + label_current_status: Status sekarang + label_new_statuses_allowed: Status baru yang diijinkan + label_all: semua + label_none: tidak ada + label_nobody: tidak ada + label_next: Berikut + label_previous: Sebelum + label_used_by: Digunakan oleh + label_details: Rincian + label_add_note: Tambahkan catatan + label_per_page: Per halaman + label_calendar: Kalender + label_months_from: dari bulan + label_gantt: Gantt + label_internal: Internal + label_last_changes: "%{count} perubahan terakhir" + label_change_view_all: Tampilkan semua perubahan + label_personalize_page: Personalkan halaman ini + label_comment: Komentar + label_comment_plural: Komentar + label_x_comments: + zero: tak ada komentar + one: 1 komentar + other: "%{count} komentar" + label_comment_add: Tambahkan komentar + label_comment_added: Komentar ditambahkan + label_comment_delete: Hapus komentar + label_query: Custom query + label_query_plural: Custom queries + label_query_new: Query baru + label_filter_add: Tambahkan filter + label_filter_plural: Filter + label_equals: sama dengan + label_not_equals: tidak sama dengan + label_in_less_than: kurang dari + label_in_more_than: lebih dari + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: pada + label_today: hari ini + label_all_time: semua waktu + label_yesterday: kemarin + label_this_week: minggu ini + label_last_week: minggu lalu + label_last_n_days: "%{count} hari terakhir" + label_this_month: bulan ini + label_last_month: bulan lalu + label_this_year: this year + label_date_range: Jangkauan tanggal + label_less_than_ago: kurang dari hari yang lalu + label_more_than_ago: lebih dari hari yang lalu + label_ago: hari yang lalu + label_contains: berisi + label_not_contains: tidak berisi + label_day_plural: hari + label_repository: Repositori + label_repository_plural: Repositori + label_browse: Jelajah + label_branch: Cabang + label_tag: Tag + label_revision: Revisi + label_revision_plural: Revisi + label_associated_revisions: Revisi terkait + label_added: ditambahkan + label_modified: diubah + label_copied: disalin + label_renamed: diganti nama + label_deleted: dihapus + label_latest_revision: Revisi terakhir + label_latest_revision_plural: Revisi terakhir + label_view_revisions: Tampilkan revisi + label_view_all_revisions: Tampilkan semua revisi + label_max_size: Ukuran maksimum + label_sort_highest: Ke paling atas + label_sort_higher: Ke atas + label_sort_lower: Ke bawah + label_sort_lowest: Ke paling bawah + label_roadmap: Rencana kerja + label_roadmap_due_in: "Harus selesai dalam %{value}" + label_roadmap_overdue: "%{value} terlambat" + label_roadmap_no_issues: Tak ada masalah pada versi ini + label_search: Cari + label_result_plural: Hasil + label_all_words: Semua kata + label_wiki: Wiki + label_wiki_edit: Sunting wiki + label_wiki_edit_plural: Sunting wiki + label_wiki_page: Halaman wiki + label_wiki_page_plural: Halaman wiki + label_index_by_title: Indeks menurut judul + label_index_by_date: Indeks menurut tanggal + label_current_version: Versi sekarang + label_preview: Tinjauan + label_feed_plural: Feeds + label_changes_details: Rincian semua perubahan + label_issue_tracking: Pelacak masalah + label_spent_time: Waktu terpakai + label_f_hour: "%{value} jam" + label_f_hour_plural: "%{value} jam" + label_time_tracking: Pelacak waktu + label_change_plural: Perubahan + label_statistics: Statistik + label_commits_per_month: Komit per bulan + label_commits_per_author: Komit per pengarang + label_view_diff: Tampilkan perbedaan + label_diff_inline: inline + label_diff_side_by_side: berdampingan + label_options: Pilihan + label_copy_workflow_from: Salin alur kerja dari + label_permissions_report: Laporan perijinan + label_watched_issues: Masalah terpantau + label_related_issues: Masalah terkait + label_applied_status: Status teraplikasi + label_loading: Memuat... + label_relation_new: Kaitan baru + label_relation_delete: Hapus kaitan + label_relates_to: terkait pada + label_duplicates: salinan + label_duplicated_by: disalin oleh + label_blocks: blok + label_blocked_by: diblok oleh + label_precedes: mendahului + label_follows: mengikuti + label_end_to_start: akhir ke awal + label_end_to_end: akhir ke akhir + label_start_to_start: awal ke awal + label_start_to_end: awal ke akhir + label_stay_logged_in: Tetap login + label_disabled: tidak diaktifkan + label_show_completed_versions: Tampilkan versi lengkap + label_me: saya + label_board: Forum + label_board_new: Forum baru + label_board_plural: Forum + label_topic_plural: Topik + label_message_plural: Pesan + label_message_last: Pesan terakhir + label_message_new: Pesan baru + label_message_posted: Pesan ditambahkan + label_reply_plural: Balasan + label_send_information: Kirim informasi akun ke pengguna + label_year: Tahun + label_month: Bulan + label_week: Minggu + label_date_from: Dari + label_date_to: Sampai + label_language_based: Berdasarkan bahasa pengguna + label_sort_by: "Urut berdasarkan %{value}" + label_send_test_email: Kirim email percobaan + label_feeds_access_key_created_on: "Atom access key dibuat %{value} yang lalu" + label_module_plural: Modul + label_added_time_by: "Ditambahkan oleh %{author} %{age} yang lalu" + label_updated_time_by: "Diperbarui oleh %{author} %{age} yang lalu" + label_updated_time: "Diperbarui oleh %{value} yang lalu" + label_jump_to_a_project: Pilih proyek... + label_file_plural: Berkas + label_changeset_plural: Set perubahan + label_default_columns: Kolom default + label_no_change_option: (Tak ada perubahan) + label_bulk_edit_selected_issues: Ubah masalah terpilih secara masal + label_theme: Tema + label_default: Default + label_search_titles_only: Cari judul saja + label_user_mail_option_all: "Untuk semua kejadian pada semua proyek saya" + label_user_mail_option_selected: "Hanya untuk semua kejadian pada proyek yang saya pilih ..." + label_user_mail_no_self_notified: "Saya tak ingin diberitahu untuk perubahan yang saya buat sendiri" + label_user_mail_assigned_only_mail_notification: "Kirim email hanya bila saya ditugaskan untuk masalah terkait" + label_user_mail_block_mail_notification: "Saya tidak ingin menerima email. Terima kasih." + label_registration_activation_by_email: aktivasi akun melalui email + label_registration_manual_activation: aktivasi akun secara manual + label_registration_automatic_activation: aktivasi akun secara otomatis + label_display_per_page: "Per halaman: %{value}" + label_age: Umur + label_change_properties: Rincian perubahan + label_general: Umum + label_more: Lanjut + label_scm: SCM + label_plugins: Plugin + label_ldap_authentication: Otentikasi LDAP + label_downloads_abbr: Unduh + label_optional_description: Deskripsi optional + label_add_another_file: Tambahkan berkas lain + label_preferences: Preferensi + label_chronological_order: Urut sesuai kronologis + label_reverse_chronological_order: Urut dari yang terbaru + label_planning: Perencanaan + label_incoming_emails: Email masuk + label_generate_key: Buat kunci + label_issue_watchers: Pemantau + label_example: Contoh + label_display: Tampilan + label_sort: Urut + label_ascending: Menaik + label_descending: Menurun + label_date_from_to: Dari %{start} sampai %{end} + label_wiki_content_added: Halaman wiki ditambahkan + label_wiki_content_updated: Halaman wiki diperbarui + label_group: Kelompok + label_group_plural: Kelompok + label_group_new: Kelompok baru + label_time_entry_plural: Waktu terpakai + label_version_sharing_none: Tidak dibagi + label_version_sharing_descendants: Dengan subproyek + label_version_sharing_hierarchy: Dengan hirarki proyek + label_version_sharing_tree: Dengan pohon proyek + label_version_sharing_system: Dengan semua proyek + + + button_login: Login + button_submit: Kirim + button_save: Simpan + button_check_all: Contreng semua + button_uncheck_all: Hilangkan semua contreng + button_delete: Hapus + button_create: Buat + button_create_and_continue: Buat dan lanjutkan + button_test: Test + button_edit: Sunting + button_add: Tambahkan + button_change: Ubah + button_apply: Terapkan + button_clear: Bersihkan + button_lock: Kunci + button_unlock: Buka kunci + button_download: Unduh + button_list: Daftar + button_view: Tampilkan + button_move: Pindah + button_move_and_follow: Pindah dan ikuti + button_back: Kembali + button_cancel: Batal + button_activate: Aktifkan + button_sort: Urut + button_log_time: Rekam waktu + button_rollback: Kembali ke versi ini + button_watch: Pantau + button_unwatch: Tidak Memantau + button_reply: Balas + button_archive: Arsip + button_unarchive: Batalkan arsip + button_reset: Reset + button_rename: Ganti nama + button_change_password: Ubah kata sandi + button_copy: Salin + button_copy_and_follow: Salin dan ikuti + button_annotate: Anotasi + button_update: Perbarui + button_configure: Konfigur + button_quote: Kutip + button_duplicate: Duplikat + + status_active: aktif + status_registered: terdaftar + status_locked: terkunci + + version_status_open: terbuka + version_status_locked: terkunci + version_status_closed: tertutup + + field_active: Aktif + + text_select_mail_notifications: Pilih aksi dimana email notifikasi akan dikirimkan. + text_regexp_info: mis. ^[A-Z0-9]+$ + text_min_max_length_info: 0 berarti tidak ada pembatasan + text_project_destroy_confirmation: Apakah anda benar-benar akan menghapus proyek ini beserta data terkait ? + text_subprojects_destroy_warning: "Subproyek: %{value} juga akan dihapus." + text_workflow_edit: Pilih peran dan pelacak untuk menyunting alur kerja + text_are_you_sure: Anda yakin ? + text_journal_changed: "%{label} berubah dari %{old} menjadi %{new}" + text_journal_set_to: "%{label} di set ke %{value}" + text_journal_deleted: "%{label} dihapus (%{old})" + text_journal_added: "%{label} %{value} ditambahkan" + text_tip_issue_begin_day: tugas dimulai hari itu + text_tip_issue_end_day: tugas berakhir hari itu + text_tip_issue_begin_end_day: tugas dimulai dan berakhir hari itu + text_caracters_maximum: "maximum %{count} karakter." + text_caracters_minimum: "Setidaknya harus sepanjang %{count} karakter." + text_length_between: "Panjang diantara %{min} dan %{max} karakter." + text_tracker_no_workflow: Tidak ada alur kerja untuk pelacak ini + text_unallowed_characters: Karakter tidak diperbolehkan + text_comma_separated: Beberapa nilai diperbolehkan (dipisahkan koma). + text_issues_ref_in_commit_messages: Mereferensikan dan membetulkan masalah pada pesan komit + text_issue_added: "Masalah %{id} sudah dilaporkan oleh %{author}." + text_issue_updated: "Masalah %{id} sudah diperbarui oleh %{author}." + text_wiki_destroy_confirmation: Apakah anda benar-benar akan menghapus wiki ini beserta semua isinya ? + text_issue_category_destroy_question: "Beberapa masalah (%{count}) ditugaskan pada kategori ini. Apa yang anda lakukan ?" + text_issue_category_destroy_assignments: Hapus kategori penugasan + text_issue_category_reassign_to: Tugaskan kembali masalah untuk kategori ini + text_user_mail_option: "Untuk proyek yang tidak dipilih, anda hanya akan menerima notifikasi hal-hal yang anda pantau atau anda terlibat di dalamnya (misalnya masalah yang anda tulis atau ditugaskan pada anda)." + text_no_configuration_data: "Peran, pelacak, status masalah dan alur kerja belum dikonfigur.\nSangat disarankan untuk memuat konfigurasi default. Anda akan bisa mengubahnya setelah konfigurasi dimuat." + text_load_default_configuration: Muat konfigurasi default + text_status_changed_by_changeset: "Diterapkan di set perubahan %{value}." + text_issues_destroy_confirmation: 'Apakah anda yakin untuk menghapus masalah terpilih ?' + text_select_project_modules: 'Pilih modul untuk diaktifkan pada proyek ini:' + text_default_administrator_account_changed: Akun administrator default sudah berubah + text_file_repository_writable: Direktori yang bisa ditulisi untuk lampiran + text_plugin_assets_writable: Direktori yang bisa ditulisi untuk plugin asset + text_rmagick_available: RMagick tersedia (optional) + text_destroy_time_entries_question: "%{hours} jam sudah dilaporkan pada masalah yang akan anda hapus. Apa yang akan anda lakukan ?" + text_destroy_time_entries: Hapus jam yang terlapor + text_assign_time_entries_to_project: Tugaskan jam terlapor pada proyek + text_reassign_time_entries: 'Tugaskan kembali jam terlapor pada masalah ini:' + text_user_wrote: "%{value} menulis:" + text_enumeration_destroy_question: "%{count} obyek ditugaskan untuk nilai ini." + text_enumeration_category_reassign_to: 'Tugaskan kembali untuk nilai ini:' + text_email_delivery_not_configured: "Pengiriman email belum dikonfigurasi, notifikasi tidak diaktifkan.\nAnda harus mengkonfigur SMTP server anda pada config/configuration.yml dan restart kembali aplikasi untuk mengaktifkan." + text_repository_usernames_mapping: "Pilih atau perbarui pengguna Redmine yang terpetakan ke setiap nama pengguna yang ditemukan di log repositori.\nPengguna dengan nama pengguna dan repositori atau email yang sama secara otomasit akan dipetakan." + text_diff_truncated: '... Perbedaan terpotong karena melebihi batas maksimum yang bisa ditampilkan.' + text_custom_field_possible_values_info: 'Satu baris untuk setiap nilai' + text_wiki_page_destroy_question: "Halaman ini mempunyai %{descendants} halaman anak dan turunannya. Apa yang akan anda lakukan ?" + text_wiki_page_nullify_children: "Biarkan halaman anak sebagai halaman teratas (root)" + text_wiki_page_destroy_children: "Hapus halaman anak dan semua turunannya" + text_wiki_page_reassign_children: "Tujukan halaman anak ke halaman induk yang ini" + + default_role_manager: Manager + default_role_developer: Pengembang + default_role_reporter: Pelapor + default_tracker_bug: Bug + default_tracker_feature: Fitur + default_tracker_support: Dukungan + default_issue_status_new: Baru + default_issue_status_in_progress: Dalam proses + default_issue_status_resolved: Resolved + default_issue_status_feedback: Umpan balik + default_issue_status_closed: Ditutup + default_issue_status_rejected: Ditolak + default_doc_category_user: Dokumentasi pengguna + default_doc_category_tech: Dokumentasi teknis + default_priority_low: Rendah + default_priority_normal: Normal + default_priority_high: Tinggi + default_priority_urgent: Penting + default_priority_immediate: Segera + default_activity_design: Rancangan + default_activity_development: Pengembangan + + enumeration_issue_priorities: Prioritas masalah + enumeration_doc_categories: Kategori dokumen + enumeration_activities: Kegiatan + enumeration_system_activity: Kegiatan Sistem + label_copy_source: Source + label_update_issue_done_ratios: Update issue done ratios + setting_issue_done_ratio: Calculate the issue done ratio with + label_api_access_key: API access key + text_line_separated: Multiple values allowed (one line for each value). + label_revision_id: Revision %{value} + permission_view_issues: View Issues + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + label_display_used_statuses_only: Only display statuses that are used by this tracker + error_workflow_copy_target: Please select target tracker(s) and role(s) + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: Atom access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_copy_same_as_target: Same as target + button_show: Show + setting_issue_done_ratio_issue_field: Use the issue field + label_missing_api_access_key: Missing an API access key + label_copy_target: Target + label_missing_feeds_access_key: Missing a Atom access key + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + setting_start_of_week: Start calendars on + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: Commit messages 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 masalah + one: 1 masalah + other: "%{count} masalah" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: semua + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Dengan subproyek + label_cross_project_tree: Dengan pohon proyek + label_cross_project_hierarchy: Dengan hirarki proyek + label_cross_project_system: Dengan semua proyek + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Total + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1afaf3df51e175a331cbf41c5e1168bd2857b281.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1afaf3df51e175a331cbf41c5e1168bd2857b281.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1112 @@ +# translated by Dzintars Bergs (dzintars.bergs@gmail.com) + +lv: + direction: ltr + date: + formats: + default: "%d.%m.%Y" + short: "%d %b" + long: "%d %B %Y" + + day_names: [SvÄ“tdiena, Pirmdiena, Otrdiena, TreÅ¡diena, Ceturtdiena, Piektdiena, Sestdiena] + abbr_day_names: [Sv, Pr, Ot, Tr, Ct, Pk, St] + + month_names: [~, JanvÄris, FebruÄris, Marts, AprÄ«lis , Maijs, JÅ«nijs, JÅ«lijs, Augusts, Septembris, Oktobris, Novembris, Decembris] + abbr_month_names: [~, Jan, Feb, Mar, Apr, Mai, JÅ«n, JÅ«l, Aug, Sep, Okt, Nov, Dec] + order: + - :day + - :month + - :year + + time: + formats: + default: "%a, %d %b %Y, %H:%M:%S %z" + time: "%H:%M" + short: "%d %b, %H:%M" + long: "%B %d, %Y %H:%M" + am: "rÄ«tÄ" + pm: "vakarÄ" + + datetime: + distance_in_words: + half_a_minute: "pus minÅ«te" + less_than_x_seconds: + one: "mazÄk kÄ 1 sekunde" + other: "mazÄk kÄ %{count} sekundes" + x_seconds: + one: "1 sekunde" + other: "%{count} sekundes" + less_than_x_minutes: + one: "mazÄk kÄ minÅ«te" + other: "mazÄk kÄ %{count} minÅ«tes" + x_minutes: + one: "1 minÅ«te" + other: "%{count} minÅ«tes" + about_x_hours: + one: "aptuveni 1 stunda" + other: "aptuveni %{count} stundas" + x_hours: + one: "1 stunda" + other: "%{count} stundas" + x_days: + one: "1 diena" + other: "%{count} dienas" + about_x_months: + one: "aptuveni 1 mÄ“nesis" + other: "aptuveni %{count} mÄ“neÅ¡i" + x_months: + one: "1 mÄ“nesis" + other: "%{count} mÄ“neÅ¡i" + about_x_years: + one: "aptuveni 1 gads" + other: "aptuveni %{count} gadi" + over_x_years: + one: "ilgÄk par 1 gadu" + other: "ilgÄk par %{count} gadiem" + almost_x_years: + one: "gandrÄ«z 1 gadu" + other: "gandrÄ«z %{count} gadus" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: " " + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Baits" + other: "Baiti" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: + sentence_connector: "un" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "nav iekļauts sarakstÄ" + exclusion: "ir rezervÄ“ts" + invalid: "nederÄ«gs" + confirmation: "apstiprinÄjums nesakrÄ«t" + accepted: "jÄbÅ«t akceptÄ“tam" + empty: "nevar bÅ«t tukÅ¡s" + blank: "nevar bÅ«t neaizpildÄ«ts" + too_long: "ir pÄrÄk gara(Å¡) (maksimÄlais garums ir %{count} simboli)" + too_short: "ir pÄrÄk Ä«sa(s) (minimÄlais garums ir %{count} simboli)" + wrong_length: "ir nepareiza garuma (vajadzÄ“tu bÅ«t %{count} simboli)" + taken: "eksistÄ“" + not_a_number: "nav skaitlis" + not_a_date: "nav derÄ«gs datums" + greater_than: "jÄbÅ«t lielÄkam par %{count}" + greater_than_or_equal_to: "jÄbÅ«t lielÄkam vai vienÄdam ar %{count}" + equal_to: "jÄbÅ«t vienÄdam ar %{count}" + less_than: "jÄbÅ«t mazÄkam kÄ %{count}" + less_than_or_equal_to: "jÄbÅ«t mazÄkam vai vienÄdam ar %{count}" + odd: "jÄatšķirÄs" + even: "jÄsakrÄ«t" + greater_than_start_date: "jÄbÅ«t vÄ“lÄkam par sÄkuma datumu" + not_same_project: "nepieder pie tÄ paÅ¡a projekta" + circular_dependency: "Å Ä« relÄcija radÄ«tu ciklisku atkarÄ«bu" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: IzvÄ“lieties + + general_text_No: 'NÄ“' + general_text_Yes: 'JÄ' + general_text_no: 'nÄ“' + general_text_yes: 'jÄ' + general_lang_name: 'Latvian (LatvieÅ¡u)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Konts tika atjaunots veiksmÄ«gi. + notice_account_invalid_creditentials: Nepareizs lietotÄja vÄrds vai parole. + notice_account_password_updated: Parole tika veiksmÄ«gi atjaunota. + notice_account_wrong_password: Nepareiza parole + notice_account_register_done: Konts veiksmÄ«gi izveidots. Lai aktivizÄ“tu kontu, spiediet uz saites, kas Jums tika nosÅ«tÄ«ta. + notice_account_unknown_email: NezinÄms lietotÄjs + notice_can_t_change_password: Å is konts izmanto ÄrÄ“ju pilnvaroÅ¡anas avotu. Nav iespÄ“jams nomainÄ«t paroli. + notice_account_lost_email_sent: Jums tika nosÅ«tÄ«ts e-pasts ar instrukcijÄm, kÄ izveidot jaunu paroli. + notice_account_activated: JÅ«su konts ir aktivizÄ“ts. Varat pieslÄ“gties sistÄ“mai. + notice_successful_create: VeiksmÄ«ga izveide. + notice_successful_update: VeiksmÄ«ga atjaunoÅ¡ana. + notice_successful_delete: VeiksmÄ«ga dzēšana. + notice_successful_connection: VeiksmÄ«gs savienojums. + notice_file_not_found: Lapa, ko JÅ«s mēģinÄt atvÄ“rt, neeksistÄ“ vai ir pÄrvietota. + notice_locking_conflict: Datus ir atjaunojis cits lietotÄjs. + notice_not_authorized: Jums nav tiesÄ«bu piekļūt Å¡ai lapai. + notice_email_sent: "E-pasts tika nosÅ«tÄ«ts uz %{value}" + notice_email_error: "Kļūda sÅ«tot e-pastu (%{value})" + notice_feeds_access_key_reseted: JÅ«su Atom pieejas atslÄ“ga tika iestatÄ«ta sÄkuma stÄvoklÄ«. + notice_api_access_key_reseted: JÅ«su API pieejas atslÄ“ga tika iestatÄ«ta sÄkuma stÄvoklÄ«. + notice_failed_to_save_issues: "NeizdevÄs saglabÄt %{count} uzdevumu(us) no %{total} izvÄ“lÄ“ti: %{ids}." + notice_no_issue_selected: "Nav izvÄ“lÄ“ts uzdevums! LÅ«dzu, atzÄ«mÄ“jiet uzdevumus, kurus vÄ“laties rediģēt!" + notice_account_pending: "JÅ«su konts tika izveidots un Å¡obrÄ«d gaida administratora apstiprinÄjumu." + notice_default_data_loaded: NoklusÄ“tÄ konfigurÄcija tika veiksmÄ«gi ielÄdÄ“ta. + notice_unable_delete_version: NeizdevÄs dzÄ“st versiju. + notice_issue_done_ratios_updated: Uzdevuma izpildes koeficients atjaunots. + + error_can_t_load_default_data: "Nevar ielÄdÄ“t noklusÄ“tos konfigurÄcijas datus: %{value}" + error_scm_not_found: "Ieraksts vai versija nebija repozitorijÄ." + error_scm_command_failed: "Mēģinot piekļūt repozitorijam, notika kļūda: %{value}" + error_scm_annotate: "Ieraksts neeksistÄ“ vai tam nevar tikt pievienots paskaidrojums." + error_issue_not_found_in_project: 'Uzdevums netika atrasts vai nepieder Å¡im projektam.' + error_no_tracker_in_project: 'Neviens trakeris nav saistÄ«ts ar Å¡o projektu. PÄrbaudiet projekta iestatÄ«jumus.' + error_no_default_issue_status: 'Nav definÄ“ts uzdevuma noklusÄ“tais statuss. PÄrbaudiet konfigurÄciju (Ejat uz: "AdministrÄcija -> Uzdevumu statusi")!' + error_can_not_reopen_issue_on_closed_version: 'Nevar pievienot atsauksmi uzdevumam, kas saistÄ«ts ar slÄ“gtu versiju.' + error_can_not_archive_project: Å is projekts nevar tikt arhivÄ“ts + error_issue_done_ratios_not_updated: "Uzdevuma izpildes koeficients nav atjaunots." + error_workflow_copy_source: 'LÅ«dzu izvÄ“lieties avota trakeri vai lomu' + error_workflow_copy_target: 'LÅ«dzu izvÄ“lÄ“ties mÄ“rÄ·a trakeri(us) un lomu(as)' + + warning_attachments_not_saved: "%{count} datnes netika saglabÄtas." + + mail_subject_lost_password: "JÅ«su %{value} parole" + mail_body_lost_password: 'Lai mainÄ«tu paroli, spiediet uz šīs saites:' + mail_subject_register: "JÅ«su %{value} konta aktivizÄcija" + mail_body_register: 'Lai izveidotu kontu, spiediet uz šīs saites:' + mail_body_account_information_external: "Varat izmantot JÅ«su %{value} kontu, lai pieslÄ“gtos." + mail_body_account_information: JÅ«su konta informÄcija + mail_subject_account_activation_request: "%{value} konta aktivizÄcijas pieprasÄ«jums" + mail_body_account_activation_request: "Jauns lietotÄjs (%{value}) ir reÄ£istrÄ“ts. LietotÄja konts gaida JÅ«su apstiprinÄjumu:" + mail_subject_reminder: "%{count} uzdevums(i) sagaidÄms(i) tuvÄkajÄs %{days} dienÄs" + mail_body_reminder: "%{count} uzdevums(i), kurÅ¡(i) ir nozÄ«mÄ“ts(i) Jums, sagaidÄms(i) tuvÄkajÄs %{days} dienÄs:" + mail_subject_wiki_content_added: "'%{id}' Wiki lapa pievienota" + mail_body_wiki_content_added: "The '%{id}' Wiki lapu pievienojis %{author}." + mail_subject_wiki_content_updated: "'%{id}' Wiki lapa atjaunota" + mail_body_wiki_content_updated: "The '%{id}' Wiki lapu atjaunojis %{author}." + + + field_name: Nosaukums + field_description: Apraksts + field_summary: Kopsavilkums + field_is_required: NepiecieÅ¡ams + field_firstname: VÄrds + field_lastname: UzvÄrds + field_mail: "E-pasts" + field_filename: Datne + field_filesize: IzmÄ“rs + field_downloads: LejupielÄdes + field_author: Autors + field_created_on: Izveidots + field_updated_on: Atjaunots + field_field_format: FormÄts + field_is_for_all: Visiem projektiem + field_possible_values: IespÄ“jamÄs vÄ“rtÄ«bas + field_regexp: RegulÄrÄ izteiksme + field_min_length: MinimÄlais garums + field_max_length: MaksimÄlais garums + field_value: VÄ“rtÄ«ba + field_category: Kategorija + field_title: Nosaukums + field_project: Projekts + field_issue: Uzdevums + field_status: Statuss + field_notes: PiezÄ«mes + field_is_closed: Uzdevums slÄ“gts + field_is_default: NoklusÄ“tÄ vÄ“rtÄ«ba + field_tracker: Trakeris + field_subject: Temats + field_due_date: SagaidÄmais datums + field_assigned_to: Piešķirts + field_priority: PrioritÄte + field_fixed_version: MÄ“rÄ·a versija + field_user: LietotÄjs + field_role: Loma + field_homepage: Vietne + field_is_public: Publisks + field_parent: ApakÅ¡projekts projektam + field_is_in_roadmap: CeļvedÄ« parÄdÄ«tie uzdevumi + field_login: PieslÄ“gties + field_mail_notification: "E-pasta paziņojumi" + field_admin: Administrators + field_last_login_on: PÄ“dÄ“jo reizi pieslÄ“dzies + field_language: Valoda + field_effective_date: Datums + field_password: Parole + field_new_password: JanÄ parole + field_password_confirmation: Paroles apstiprinÄjums + field_version: Versija + field_type: Tips + field_host: Hosts + field_port: Ports + field_account: Konts + field_base_dn: Base DN + field_attr_login: PieslÄ“gÅ¡anÄs atribÅ«ts + field_attr_firstname: VÄrda atribÅ«ts + field_attr_lastname: UzvÄrda atribÅ«ts + field_attr_mail: "E-pasta atribÅ«ts" + field_onthefly: "LietotÄja izveidoÅ¡ana on-the-fly" + field_start_date: SÄkuma datums + field_done_ratio: "% padarÄ«ti" + field_auth_source: PilnvaroÅ¡anas režīms + field_hide_mail: "PaslÄ“pt manu e-pasta adresi" + field_comments: KomentÄrs + field_url: URL + field_start_page: SÄkuma lapa + field_subproject: ApakÅ¡projekts + field_hours: Stundas + field_activity: AktivitÄte + field_spent_on: Datums + field_identifier: Identifikators + field_is_filter: Izmantots kÄ filtrs + field_issue_to: SaistÄ«ts uzdevums + field_delay: KavÄ“jums + field_assignable: Uzdevums var tikt piesaistÄ«ts Å¡ai lomai + field_redirect_existing_links: PÄradresÄ“t eksistÄ“joÅ¡Äs saites + field_estimated_hours: ParedzÄ“tais laiks + field_column_names: Kolonnas + field_time_zone: Laika zona + field_searchable: MeklÄ“jams + field_default_value: NoklusÄ“tÄ vÄ“rtÄ«ba + field_comments_sorting: RÄdÄ«t komentÄrus + field_parent_title: VecÄka lapa + field_editable: Rediģējams + field_watcher: VÄ“rotÄjs + field_identity_url: OpenID URL + field_content: Saturs + field_group_by: GrupÄ“t rezultÄtus pÄ“c + field_sharing: KoplietoÅ¡ana + + setting_app_title: Programmas nosaukums + setting_app_subtitle: Programmas apakÅ¡-nosaukums + setting_welcome_text: Sveiciena teksts + setting_default_language: NoklusÄ“tÄ valoda + setting_login_required: NepiecieÅ¡ama pilnvaroÅ¡ana + setting_self_registration: PaÅ¡reÄ£istrēšanÄs + setting_attachment_max_size: Pielikuma maksimÄlais izmÄ“rs + setting_issues_export_limit: Uzdevumu eksporta ierobežojums + setting_mail_from: "E-pasta adrese informÄcijas nosÅ«tīšanai" + setting_bcc_recipients: "SaņēmÄ“ju adreses neparÄdÄ«sies citu saņēmÄ“ju vÄ“stulÄ“s (bcc)" + setting_plain_text_mail: "VÄ“stule brÄ«vÄ tekstÄ (bez HTML)" + setting_host_name: Hosta nosaukums un piekļuves ceļš + setting_text_formatting: Teksta formatēšana + setting_wiki_compression: Wiki vÄ“stures saspieÅ¡ana + setting_feeds_limit: Barotnes satura ierobežojums + setting_default_projects_public: Jaunie projekti noklusÄ“ti ir publiski pieejami + setting_autofetch_changesets: "AutomÄtiski lietot jaunÄko versiju, pieslÄ“dzoties repozitorijam (Autofetch)" + setting_sys_api_enabled: IeslÄ“gt WS repozitoriju menedžmentam + setting_commit_ref_keywords: NorÄdes atslÄ“gvÄrdi + setting_commit_fix_keywords: FiksÄ“joÅ¡ie atslÄ“gvÄrdi + setting_autologin: AutomÄtiskÄ pieslÄ“gÅ¡anÄs + setting_date_format: Datuma formÄts + setting_time_format: Laika formÄts + setting_cross_project_issue_relations: "Atļaut starp-projektu uzdevumu relÄcijas" + setting_issue_list_default_columns: NoklusÄ“ti rÄdÄ«tÄs kolonnas uzdevumu sarakstÄ + setting_emails_footer: "E-pastu kÄjene" + setting_protocol: Protokols + setting_per_page_options: Objekti vienÄ lapÄ + setting_user_format: LietotÄju rÄdīšanas formÄts + setting_activity_days_default: Dienus skaits aktivitÄÅ¡u rÄdīšanai aktivitÄÅ¡u sadaÄ¼Ä + setting_display_subprojects_issues: RÄdÄ«t apakÅ¡projekta uzdevumus galvenajÄ projektÄ pÄ“c noklusÄ“juma + setting_enabled_scm: Lietot SCM + setting_mail_handler_body_delimiters: "SaÄ«sinÄt pÄ“c vienas no Å¡im rindÄm" + setting_mail_handler_api_enabled: "Lietot WS ienÄkoÅ¡ajiem e-pastiem" + setting_mail_handler_api_key: API atslÄ“ga + setting_sequential_project_identifiers: Ä¢enerÄ“t secÄ«gus projektu identifikatorus + setting_gravatar_enabled: Izmantot Gravatar lietotÄju ikonas + setting_gravatar_default: NoklusÄ“tais Gravatar attÄ“ls + setting_diff_max_lines_displayed: MaksimÄlais rÄdÄ«to diff rindu skaits + setting_file_max_size_displayed: MaksimÄlais izmÄ“rs iekļautajiem teksta failiem + setting_repository_log_display_limit: MaksimÄlais žurnÄla datnÄ“ rÄdÄ«to revÄ«ziju skaits + setting_openid: Atļaut OpenID pieslÄ“gÅ¡anos un reÄ£istrēšanos + setting_password_min_length: MinimÄlais paroles garums + setting_new_project_user_role_id: Loma, kura tiek piešķirta ne-administratora lietotÄjam, kurÅ¡ izveido projektu + setting_default_projects_modules: NoklusÄ“tie lietotie moduļi jaunam projektam + setting_issue_done_ratio: AprēķinÄt uzdevuma izpildes koeficientu ar + setting_issue_done_ratio_issue_field: uzdevuma lauku + setting_issue_done_ratio_issue_status: uzdevuma statusu + setting_start_of_week: SÄkt kalendÄru ar + setting_rest_api_enabled: Lietot REST web-servisu + setting_cache_formatted_text: KeÅ¡ot formatÄ“tu tekstu + + permission_add_project: Izveidot projektu + permission_add_subprojects: Izveidot apakÅ¡projektu + permission_edit_project: Rediģēt projektu + permission_select_project_modules: IzvÄ“lÄ“ties projekta moduļus + permission_manage_members: PÄrvaldÄ«t dalÄ«bniekus + permission_manage_project_activities: PÄrvaldÄ«t projekta aktivitÄtes + permission_manage_versions: PÄrvaldÄ«t versijas + permission_manage_categories: PÄrvaldÄ«t uzdevumu kategorijas + permission_view_issues: ApskatÄ«t uzdevumus + permission_add_issues: Pievienot uzdevumus + permission_edit_issues: Rediģēt uzdevumus + permission_manage_issue_relations: PÄrvaldÄ«t uzdevumu relÄcijas + permission_add_issue_notes: Pievienot piezÄ«mes + permission_edit_issue_notes: Rediģēt piezÄ«mes + permission_edit_own_issue_notes: Rediģēt paÅ¡a piezÄ«mes + permission_move_issues: PÄrvietot uzdevumus + permission_delete_issues: DzÄ“st uzdevumus + permission_manage_public_queries: PÄrvaldÄ«t publiskos pieprasÄ«jumus + permission_save_queries: SaglabÄt pieprasÄ«jumus + permission_view_gantt: SkatÄ«t Ganta diagrammu + permission_view_calendar: SkatÄ«t kalendÄru + permission_view_issue_watchers: SkatÄ«t vÄ“rotÄju sarakstu + permission_add_issue_watchers: Pievienot vÄ“rotÄjus + permission_delete_issue_watchers: DzÄ“st vÄ“rotÄjus + permission_log_time: PiereÄ£istrÄ“t pavadÄ«to laiku + permission_view_time_entries: SkatÄ«t pavadÄ«to laiku + permission_edit_time_entries: Rdiģēt laika reÄ£istrus + permission_edit_own_time_entries: Rediģēt savus laika reÄ£istrus + permission_manage_news: PÄrvaldÄ«t jaunumus + permission_comment_news: KomentÄ“t jaunumus + permission_view_documents: SkatÄ«t dokumentus + permission_manage_files: PÄrvaldÄ«t failus + permission_view_files: SkatÄ«t failus + permission_manage_wiki: PÄrvaldÄ«t wiki + permission_rename_wiki_pages: PÄrsaukt wiki lapas + permission_delete_wiki_pages: DzÄ“st wiki lapas + permission_view_wiki_pages: SkatÄ«t wiki + permission_view_wiki_edits: SkatÄ«t wiki vÄ“sturi + permission_edit_wiki_pages: Rdiģēt wiki lapas + permission_delete_wiki_pages_attachments: DzÄ“st pielikumus + permission_protect_wiki_pages: Projekta wiki lapas + permission_manage_repository: PÄrvaldÄ«t repozitoriju + permission_browse_repository: PÄrlÅ«kot repozitoriju + permission_view_changesets: SkatÄ«t izmaiņu kopumus + permission_commit_access: Atļaut piekļuvi + permission_manage_boards: PÄrvaldÄ«t ziņojumu dēļus + permission_view_messages: SkatÄ«t ziņas + permission_add_messages: PublicÄ“t ziņas + permission_edit_messages: Rediģēt ziņas + permission_edit_own_messages: Rediģēt savas ziņas + permission_delete_messages: DzÄ“st ziņas + permission_delete_own_messages: DzÄ“st savas ziņas + permission_export_wiki_pages: EksportÄ“t Wiki lapas + + project_module_issue_tracking: Uzdevumu uzskaite + project_module_time_tracking: Laika uzskaite + project_module_news: Jaunumi + project_module_documents: Dokumenti + project_module_files: Datnes + project_module_wiki: Wiki + project_module_repository: Repozitorijs + project_module_boards: Ziņojumu dēļi + + label_user: LietotÄjs + label_user_plural: LietotÄji + label_user_new: Jauns lietotÄjs + label_user_anonymous: AnonÄ«ms + label_project: Projekts + label_project_new: Jauns projekts + label_project_plural: Projekti + label_x_projects: + zero: nav projektu + one: 1 projekts + other: "%{count} projekti" + label_project_all: Visi projekti + label_project_latest: JaunÄkie projekti + label_issue: Uzdevums + label_issue_new: Jauns uzdevums + label_issue_plural: Uzdevumi + label_issue_view_all: SkatÄ«t visus uzdevumus + label_issues_by: "KÄrtot pÄ“c %{value}" + label_issue_added: Uzdevums pievienots + label_issue_updated: Uzdevums atjaunots + label_document: Dokuments + label_document_new: Jauns dokuments + label_document_plural: Dokumenti + label_document_added: Dokuments pievienots + label_role: Loma + label_role_plural: Lomas + label_role_new: Jauna loma + label_role_and_permissions: Lomas un atļaujas + label_member: DalÄ«bnieks + label_member_new: Jauns dalÄ«bnieks + label_member_plural: DalÄ«bnieki + label_tracker: Trakeris + label_tracker_plural: Trakeri + label_tracker_new: Jauns trakeris + label_workflow: Darba gaita + label_issue_status: Uzdevuma statuss + label_issue_status_plural: Uzdevumu statusi + label_issue_status_new: Jauns statuss + label_issue_category: Uzdevuma kategorija + label_issue_category_plural: Uzdevumu kategorijas + label_issue_category_new: Jauna kategorija + label_custom_field: PielÄgojams lauks + label_custom_field_plural: PielÄgojami lauki + label_custom_field_new: Jauns pielÄgojams lauks + label_enumerations: UzskaitÄ«jumi + label_enumeration_new: Jauna vÄ“rtÄ«ba + label_information: InformÄcija + label_information_plural: InformÄcija + label_please_login: LÅ«dzu pieslÄ“dzieties + label_register: ReÄ£istrÄ“ties + label_login_with_open_id_option: vai pieslÄ“gties ar OpenID + label_password_lost: NozaudÄ“ta parole + label_home: SÄkums + label_my_page: Mana lapa + label_my_account: Mans konts + label_my_projects: Mani projekti + label_administration: AdministrÄcija + label_login: PieslÄ“gties + label_logout: AtslÄ“gties + label_help: PalÄ«dzÄ«ba + label_reported_issues: Ziņotie uzdevumi + label_assigned_to_me_issues: Man piesaistÄ«tie uzdevumi + label_last_login: PÄ“dÄ“jÄ pieslÄ“gÅ¡anÄs + label_registered_on: ReÄ£istrÄ“jies + label_activity: AktivitÄte + label_overall_activity: KopÄ“jÄs aktivitÄtes + label_user_activity: "LietotÄja %{value} aktivitÄtes" + label_new: Jauns + label_logged_as: PieslÄ“dzies kÄ + label_environment: Vide + label_authentication: PilnvaroÅ¡ana + label_auth_source: PilnvaroÅ¡anas režīms + label_auth_source_new: Jauns pilnvaroÅ¡anas režīms + label_auth_source_plural: PilnvaroÅ¡anas režīmi + label_subproject_plural: ApakÅ¡projekti + label_subproject_new: Jauns apakÅ¡projekts + label_and_its_subprojects: "%{value} un tÄ apakÅ¡projekti" + label_min_max_length: MinimÄlais - MaksimÄlais garums + label_list: Saraksts + label_date: Datums + label_integer: Vesels skaitlis + label_float: DecimÄlskaitlis + label_boolean: Patiesuma vÄ“rtÄ«ba + label_string: Teksts + label_text: GarÅ¡ teksts + label_attribute: AtribÅ«ts + label_attribute_plural: AtribÅ«ti + label_no_data: Nav datu, ko parÄdÄ«t + label_change_status: MainÄ«t statusu + label_history: VÄ“sture + label_attachment: Pielikums + label_attachment_new: Jauns pielikums + label_attachment_delete: DzÄ“st pielikumu + label_attachment_plural: Pielikumi + label_file_added: Lauks pievienots + label_report: Atskaite + label_report_plural: Atskaites + label_news: Ziņa + label_news_new: Pievienot ziņu + label_news_plural: Ziņas + label_news_latest: JaunÄkÄs ziņas + label_news_view_all: SkatÄ«t visas ziņas + label_news_added: Ziņas pievienotas + label_settings: IestatÄ«jumi + label_overview: PÄrskats + label_version: Versija + label_version_new: Jauna versija + label_version_plural: Versijas + label_close_versions: AizvÄ“rt pabeigtÄs versijas + label_confirmation: ApstiprinÄjums + label_export_to: 'Pieejams arÄ«:' + label_read: LasÄ«t... + label_public_projects: Publiskie projekti + label_open_issues: atvÄ“rts + label_open_issues_plural: atvÄ“rti + label_closed_issues: slÄ“gts + label_closed_issues_plural: slÄ“gti + label_x_open_issues_abbr_on_total: + zero: 0 atvÄ“rti / %{total} + one: 1 atvÄ“rts / %{total} + other: "%{count} atvÄ“rti / %{total}" + label_x_open_issues_abbr: + zero: 0 atvÄ“rti + one: 1 atvÄ“rts + other: "%{count} atvÄ“rti" + label_x_closed_issues_abbr: + zero: 0 slÄ“gti + one: 1 slÄ“gts + other: "%{count} slÄ“gti" + label_total: KopÄ + label_permissions: Atļaujas + label_current_status: PaÅ¡reizÄ“jais statuss + label_new_statuses_allowed: Jauni statusi atļauti + label_all: visi + label_none: neviens + label_nobody: nekas + label_next: NÄkoÅ¡ais + label_previous: Iepriekšējais + label_used_by: Izmanto + label_details: Detaļas + label_add_note: Pievienot piezÄ«mi + label_per_page: katrÄ lapÄ + label_calendar: KalendÄrs + label_months_from: mÄ“neÅ¡i no + label_gantt: Ganta diagramma + label_internal: Iekšējais + label_last_changes: "pÄ“dÄ“jÄs %{count} izmaiņas" + label_change_view_all: SkatÄ«t visas izmaiņas + label_personalize_page: PielÄgot Å¡o lapu + label_comment: KomentÄrs + label_comment_plural: KomentÄri + label_x_comments: + zero: nav komentÄru + one: 1 komentÄrs + other: "%{count} komentÄri" + label_comment_add: Pievienot komentÄru + label_comment_added: KomentÄrs pievienots + label_comment_delete: DzÄ“st komentÄrus + label_query: PielÄgots pieprasÄ«jums + label_query_plural: PielÄgoti pieprasÄ«jumi + label_query_new: Jauns pieprasÄ«jums + label_filter_add: Pievienot filtru + label_filter_plural: Filtri + label_equals: ir + label_not_equals: nav + label_in_less_than: ir mazÄk kÄ + label_in_more_than: ir vairÄk kÄ + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: iekÅ¡ + label_today: Å¡odien + label_all_time: visu laiku + label_yesterday: vakar + label_this_week: Å¡onedēļ + label_last_week: pagÄjuÅ¡o Å¡onedēļ + label_last_n_days: "pÄ“dÄ“jÄs %{count} dienas" + label_this_month: Å¡omÄ“nes + label_last_month: pagÄjuÅ¡o mÄ“nes + label_this_year: Å¡ogad + label_date_range: Datumu apgabals + label_less_than_ago: mazÄk kÄ dienas iepriekÅ¡ + label_more_than_ago: vairÄk kÄ dienas iepriekÅ¡ + label_ago: dienas iepriekÅ¡ + label_contains: satur + label_not_contains: nesatur + label_day_plural: dienas + label_repository: Repozitorijs + label_repository_plural: Repozitoriji + label_browse: PÄrlÅ«kot + label_branch: Zars + label_tag: Birka + label_revision: RevÄ«zija + label_revision_plural: RevÄ«zijas + label_revision_id: "RevÄ«zija %{value}" + label_associated_revisions: SaistÄ«tÄs revÄ«zijas + label_added: pievienots + label_modified: modificÄ“ts + label_copied: nokopÄ“ts + label_renamed: pÄrsaukts + label_deleted: dzÄ“sts + label_latest_revision: PÄ“dÄ“jÄ revÄ«zija + label_latest_revision_plural: PÄ“dÄ“jÄs revÄ«zijas + label_view_revisions: SkatÄ«t revÄ«zijas + label_view_all_revisions: SkatÄ«t visas revÄ«zijas + label_max_size: MaksimÄlais izmÄ“rs + label_sort_highest: PÄrvietot uz augÅ¡u + label_sort_higher: PÄrvietot soli augÅ¡up + label_sort_lower: PÄrvietot uz leju + label_sort_lowest: PÄrvietot vienu soli uz leju + label_roadmap: Ceļvedis + label_roadmap_due_in: "SagaidÄms pÄ“c %{value}" + label_roadmap_overdue: "nokavÄ“ts %{value}" + label_roadmap_no_issues: Å ai versijai nav uzdevumu + label_search: MeklÄ“t + label_result_plural: RezultÄti + label_all_words: Visi vÄrdi + label_wiki: Wiki + label_wiki_edit: Wiki labojums + label_wiki_edit_plural: Wiki labojumi + label_wiki_page: Wiki lapa + label_wiki_page_plural: Wiki lapas + label_index_by_title: IndeksÄ“t pÄ“c nosaukuma + label_index_by_date: IndeksÄ“t pÄ“c datuma + label_current_version: TekoÅ¡Ä versija + label_preview: PriekÅ¡skatÄ«jums + label_feed_plural: Barotnes + label_changes_details: Visu izmaiņu detaļas + label_issue_tracking: Uzdevumu uzskaite + label_spent_time: PavadÄ«tais laiks + label_f_hour: "%{value} stunda" + label_f_hour_plural: "%{value} stundas" + label_time_tracking: Laika uzskaite + label_change_plural: Izmaiņas + label_statistics: Statistika + label_commits_per_month: Nodevumi mÄ“nesÄ« + label_commits_per_author: Nodevumi no autora + label_view_diff: SkatÄ«t atšķirÄ«bas + label_diff_inline: iekļauts + label_diff_side_by_side: blakus + label_options: Opcijas + label_copy_workflow_from: KopÄ“t darba plÅ«smu no + label_permissions_report: Atļauju atskaite + label_watched_issues: VÄ“rotie uzdevumi + label_related_issues: SaistÄ«tie uzdevumi + label_applied_status: Piešķirtais statuss + label_loading: LÄdÄ“jas... + label_relation_new: Jauna relÄcija + label_relation_delete: DzÄ“st relÄciju + label_relates_to: saistÄ«ts ar + label_duplicates: dublikÄti + label_duplicated_by: dublÄ“jas ar + label_blocks: bloÄ·Ä“ + label_blocked_by: nobloÄ·Ä“jis + label_precedes: pirms + label_follows: seko + label_end_to_start: no beigÄm uz sÄkumu + label_end_to_end: no beigÄm uz beigÄm + label_start_to_start: no sÄkuma uz sÄkumu + label_start_to_end: no sÄkuma uz beigÄm + label_stay_logged_in: AtcerÄ“ties mani + label_disabled: izslÄ“gts + label_show_completed_versions: RÄdÄ«t pabeigtÄs versijas + label_me: es + label_board: Forums + label_board_new: Jauns forums + label_board_plural: Forumi + label_board_locked: SlÄ“gts + label_board_sticky: SvarÄ«gs + label_topic_plural: TÄ“mas + label_message_plural: Ziņas + label_message_last: PÄ“dÄ“jÄ ziņa + label_message_new: Jauna ziņa + label_message_posted: Ziņa pievienota + label_reply_plural: Atbildes + label_send_information: SÅ«tÄ«t konta informÄciju lietotÄjam + label_year: Gads + label_month: MÄ“nesis + label_week: Nedēļa + label_date_from: No + label_date_to: Kam + label_language_based: Izmantot lietotÄja valodu + label_sort_by: "KÄrtot pÄ“c %{value}" + label_send_test_email: "SÅ«tÄ«t testa e-pastu" + label_feeds_access_key: Atom piekļuves atslÄ“ga + label_missing_feeds_access_key: TrÅ«kst Atom piekļuves atslÄ“gas + label_feeds_access_key_created_on: "Atom piekļuves atslÄ“ga izveidota pirms %{value}" + label_module_plural: Moduļi + label_added_time_by: "Pievienojis %{author} pirms %{age}" + label_updated_time_by: "Atjaunojis %{author} pirms %{age}" + label_updated_time: "Atjaunots pirms %{value}" + label_jump_to_a_project: PÄriet uz projektu... + label_file_plural: Datnes + label_changeset_plural: Izmaiņu kopumi + label_default_columns: NoklusÄ“tÄs kolonnas + label_no_change_option: (Nav izmaiņu) + label_bulk_edit_selected_issues: Labot visus izvÄ“lÄ“tos uzdevumus + label_theme: TÄ“ma + label_default: NoklusÄ“ts + label_search_titles_only: MeklÄ“t tikai nosaukumos + label_user_mail_option_all: "Par visiem notikumiem visos manos projektos" + label_user_mail_option_selected: "Par visiem notikumiem tikai izvÄ“lÄ“tajos projektos..." + label_user_mail_no_self_notified: "Neziņot man par izmaiņÄm, kuras veicu es pats" + label_registration_activation_by_email: "konta aktivizÄcija caur e-pastu" + label_registration_manual_activation: manuÄlÄ konta aktivizÄcija + label_registration_automatic_activation: automÄtiskÄ konta aktivizÄcija + label_display_per_page: "RÄdÄ«t vienÄ lapÄ: %{value}" + label_age: Vecums + label_change_properties: MainÄ«t atribÅ«tus + label_general: Galvenais + label_more: VÄ“l + label_scm: SCM + label_plugins: Spraudņi + label_ldap_authentication: LDAP pilnvaroÅ¡ana + label_downloads_abbr: L-lÄd. + label_optional_description: "Apraksts (neobligÄts)" + label_add_another_file: Pievienot citu failu + label_preferences: PriekÅ¡rocÄ«bas + label_chronological_order: HronoloÄ£iskÄ kÄrtÄ«bÄ + label_reverse_chronological_order: Apgriezti hronoloÄ£iskÄ kÄrtÄ«bÄ + label_planning: PlÄnoÅ¡ana + label_incoming_emails: "IenÄkoÅ¡ie e-pasti" + label_generate_key: Ä¢enerÄ“t atslÄ“gu + label_issue_watchers: VÄ“rotÄji + label_example: PiemÄ“rs + label_display: RÄdÄ«t + label_sort: KÄrtot + label_ascending: AugoÅ¡i + label_descending: DilstoÅ¡i + label_date_from_to: "No %{start} lÄ«dz %{end}" + label_wiki_content_added: Wiki lapa pievienota + label_wiki_content_updated: Wiki lapa atjaunota + label_group: Grupa + label_group_plural: Grupas + label_group_new: Jauna grupa + label_time_entry_plural: PavadÄ«tais laiks + label_version_sharing_none: Nav koplietoÅ¡anai + label_version_sharing_descendants: Ar apakÅ¡projektiem + label_version_sharing_hierarchy: Ar projektu hierarhiju + label_version_sharing_tree: Ar projekta koku + label_version_sharing_system: Ar visiem projektiem + label_update_issue_done_ratios: Atjaunot uzdevuma veikuma attiecÄ«bu + label_copy_source: Avots + label_copy_target: MÄ“rÄ·is + label_copy_same_as_target: TÄds pats kÄ mÄ“rÄ·is + label_display_used_statuses_only: "RÄdÄ«t tikai statusus, ko lieto Å¡is trakeris" + label_api_access_key: API pieejas atslÄ“ga + label_missing_api_access_key: TrÅ«kst API pieejas atslÄ“ga + label_api_access_key_created_on: "API pieejas atslÄ“ga izveidota pirms %{value}" + + button_login: PieslÄ“gties + button_submit: NosÅ«tÄ«t + button_save: SaglabÄt + button_check_all: AtzÄ«mÄ“t visu + button_uncheck_all: Noņemt visus atzÄ«mÄ“jumus + button_delete: DzÄ“st + button_create: Izveidot + button_create_and_continue: Izveidot un turpinÄt + button_test: TestÄ“t + button_edit: Labot + button_add: Pievienot + button_change: MainÄ«t + button_apply: ApstiprinÄt + button_clear: NotÄ«rÄ«t + button_lock: SlÄ“gt + button_unlock: AtslÄ“gt + button_download: LejuplÄdÄ“t + button_list: Saraksts + button_view: Skats + button_move: PÄrvietot + button_move_and_follow: PÄrvietot un sekot + button_back: Atpakaļ + button_cancel: Atcelt + button_activate: AktivizÄ“t + button_sort: KÄrtot + button_log_time: ReÄ£istrÄ“t laiku + button_rollback: Atjaunot uz Å¡o versiju + button_watch: VÄ“rot + button_unwatch: NevÄ“rot + button_reply: AtbildÄ“t + button_archive: ArhivÄ“t + button_unarchive: AtarhivÄ“t + button_reset: AtiestatÄ«t + button_rename: PÄrsaukt + button_change_password: MainÄ«t paroli + button_copy: KopÄ“t + button_copy_and_follow: KopÄ“t un sekot + button_annotate: PierakstÄ«t paskaidrojumu + button_update: Atjaunot + button_configure: KonfigurÄ“t + button_quote: CitÄts + button_duplicate: DublÄ“t + button_show: RÄdÄ«t + + status_active: aktÄ«vs + status_registered: reÄ£istrÄ“ts + status_locked: slÄ“gts + + version_status_open: atvÄ“rta + version_status_locked: slÄ“gta + version_status_closed: aizvÄ“rta + + field_active: AktÄ«vs + + text_select_mail_notifications: "IzvÄ“lieties darbÄ«bas, par kurÄm vÄ“laties saņemt ziņojumus e-pastÄ" + text_regexp_info: "piem. ^[A-Z0-9]+$" + text_min_max_length_info: "0 nozÄ«mÄ“, ka nav ierobežojumu" + text_project_destroy_confirmation: "Vai tieÅ¡Äm vÄ“laties dzÄ“st Å¡o projektu un ar to saistÄ«tos datus?" + text_subprojects_destroy_warning: "TÄ apakÅ¡projekts(i): %{value} arÄ« tiks dzÄ“sts(i)." + text_workflow_edit: Lai labotu darba plÅ«smu, izvÄ“lieties lomu un trakeri + text_are_you_sure: "Vai esat pÄrliecinÄts?" + text_journal_changed: "%{label} mainÄ«ts no %{old} uz %{new}" + text_journal_set_to: "%{label} iestatÄ«ts uz %{value}" + text_journal_deleted: "%{label} dzÄ“sts (%{old})" + text_journal_added: "%{label} %{value} pievienots" + text_tip_issue_begin_day: uzdevums sÄkas Å¡odien + text_tip_issue_end_day: uzdevums beidzas Å¡odien + text_tip_issue_begin_end_day: uzdevums sÄkas un beidzas Å¡odien + text_caracters_maximum: "%{count} simboli maksimÄli." + text_caracters_minimum: "JÄbÅ«t vismaz %{count} simbolu garumÄ." + text_length_between: "Garums starp %{min} un %{max} simboliem." + text_tracker_no_workflow: Å im trakerim nav definÄ“ta darba plÅ«sma + text_unallowed_characters: Neatļauti simboli + text_comma_separated: "Atļautas vairÄkas vÄ“rtÄ«bas (atdalÄ«t ar komatu)." + text_line_separated: "Atļautas vairÄkas vÄ“rtÄ«bas (rakstÄ«t katru savÄ rindÄ)." + text_issues_ref_in_commit_messages: "Izmaiņu salÄ«dzinÄÅ¡ana izejot no ziņojumiem" + text_issue_added: "Uzdevumu %{id} pievienojis %{author}." + text_issue_updated: "Uzdevumu %{id} atjaunojis %{author}." + text_wiki_destroy_confirmation: "Vai esat droÅ¡s, ka vÄ“laties dzÄ“st Å¡o wiki un visu tÄs saturu?" + text_issue_category_destroy_question: "Daži uzdevumi (%{count}) ir nozÄ«mÄ“ti Å¡ai kategorijai. Ko JÅ«s vÄ“laties darÄ«t?" + text_issue_category_destroy_assignments: DzÄ“st kategoriju nozÄ«mÄ“jumus + text_issue_category_reassign_to: NozÄ«mÄ“t uzdevumus Å¡ai kategorijai + text_user_mail_option: "No neizvÄ“lÄ“tajiem projektiem JÅ«s saņemsiet ziņojumus e-pastÄ tikai par notikumiem, kuriem JÅ«s sekojat vai kuros esat iesaistÄ«ts." + text_no_configuration_data: "Lomas, trakeri, uzdevumu statusi un darba plÅ«smas vÄ“l nav konfigurÄ“tas.\nÄ»oti ieteicams ielÄdÄ“t noklusÄ“to konfigurÄciju. PÄ“c ielÄdēšanas to bÅ«s iespÄ“jams modificÄ“t." + text_load_default_configuration: IelÄdÄ“t noklusÄ“to konfigurÄciju + text_status_changed_by_changeset: "ApstiprinÄts izmaiņu kopumÄ %{value}." + text_issues_destroy_confirmation: 'Vai tieÅ¡Äm vÄ“laties dzÄ“st izvÄ“lÄ“to uzdevumu(us)?' + text_select_project_modules: 'IzvÄ“lieties moduļus Å¡im projektam:' + text_default_administrator_account_changed: NoklusÄ“tais administratora konts mainÄ«ts + text_file_repository_writable: Pielikumu direktorijÄ atļauts rakstÄ«t + text_plugin_assets_writable: Spraudņu kataloga direktorijÄ atļauts rakstÄ«t + text_rmagick_available: "RMagick pieejams (neobligÄts)" + text_destroy_time_entries_question: "%{hours} stundas tika ziņotas par uzdevumu, ko vÄ“laties dzÄ“st. Ko darÄ«t?" + text_destroy_time_entries: DzÄ“st ziņotÄs stundas + text_assign_time_entries_to_project: Piešķirt ziņotÄs stundas projektam + text_reassign_time_entries: 'Piešķirt ziņotÄs stundas uzdevumam:' + text_user_wrote: "%{value} rakstÄ«ja:" + text_enumeration_destroy_question: "%{count} objekti ir piešķirti Å¡ai vÄ“rtÄ«bai." + text_enumeration_category_reassign_to: 'Piešķirt tos Å¡ai vÄ“rtÄ«bai:' + text_email_delivery_not_configured: "E-pastu nosÅ«tīšana nav konfigurÄ“ta, un ziņojumi ir izslÄ“gti.\nKonfigurÄ“jiet savu SMTP serveri datnÄ“ config/configuration.yml un pÄrstartÄ“jiet lietotni." + text_repository_usernames_mapping: "IzvÄ“lieties vai atjaunojiet Redmine lietotÄju, saistÄ«tu ar katru lietotÄjvÄrdu, kas atrodams repozitorija žurnÄlÄ.\nLietotÄji ar to paÅ¡u Redmine un repozitorija lietotÄjvÄrdu bÅ«s saistÄ«ti automÄtiski." + text_diff_truncated: '... Å is diff tika nošķelts, jo tas pÄrsniedz maksimÄlo izmÄ“ru, ko var parÄdÄ«t.' + text_custom_field_possible_values_info: 'Katra vÄ“rtÄ«bas savÄ rindÄ' + text_wiki_page_destroy_question: "Å ij lapai ir %{descendants} apakÅ¡lapa(as) un pÄ“cnÄcÄ“ji. Ko darÄ«t?" + text_wiki_page_nullify_children: "PaturÄ“t apakÅ¡lapas kÄ pamatlapas" + text_wiki_page_destroy_children: "DzÄ“st apakÅ¡lapas un visus pÄ“cnÄcÄ“jus" + text_wiki_page_reassign_children: "Piešķirt apakÅ¡lapas Å¡ai lapai" + text_own_membership_delete_confirmation: "JÅ«s tÅ«lÄ«t dzÄ“sÄ«siet dažas vai visas atļaujas, un Jums pÄ“c tam var nebÅ«t atļauja labot Å¡o projektu.\nVai turpinÄt?" + + default_role_manager: Menedžeris + default_role_developer: IzstrÄdÄtÄjs + default_role_reporter: ZiņotÄjs + default_tracker_bug: Kļūda + default_tracker_feature: IezÄ«me + default_tracker_support: Atbalsts + default_issue_status_new: Jauns + default_issue_status_in_progress: AttÄ«stÄ«bÄ + default_issue_status_resolved: AtrisinÄts + default_issue_status_feedback: Atsauksmes + default_issue_status_closed: SlÄ“gts + default_issue_status_rejected: NoraidÄ«ts + default_doc_category_user: LietotÄja dokumentÄcija + default_doc_category_tech: TehniskÄ dokumentÄcija + default_priority_low: Zema + default_priority_normal: NormÄla + default_priority_high: Augsta + default_priority_urgent: Steidzama + default_priority_immediate: TÅ«lÄ«tÄ“ja + default_activity_design: Dizains + default_activity_development: IzstrÄdÄÅ¡ana + + enumeration_issue_priorities: Uzdevumu prioritÄtes + enumeration_doc_categories: Dokumentu kategorijas + enumeration_activities: AktivitÄtes (laika uzskaite) + enumeration_system_activity: SistÄ“mas aktivitÄtes + + error_can_not_delete_custom_field: Unable to delete custom field + permission_manage_subtasks: Manage subtasks + label_profile: Profile + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + field_parent_issue: Parent task + error_unable_delete_issue_status: Unable to delete issue status + label_subtask_plural: Subtasks + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + label_project_copy_notifications: Send email notifications during the project copy + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: KodÄ“t ziņojumus + 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 uzdevums + one: 1 uzdevums + other: "%{count} uzdevumi" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: visi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Ar apakÅ¡projektiem + label_cross_project_tree: Ar projekta koku + label_cross_project_hierarchy: Ar projektu hierarhiju + label_cross_project_system: Ar visiem projektiem + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: KopÄ + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1a/1afc6535efb2815b924fdefa8ab10dacd7ba3c18.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1afc6535efb2815b924fdefa8ab10dacd7ba3c18.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,9 @@ +class AddQueriesOptions < ActiveRecord::Migration + def up + add_column :queries, :options, :text + end + + def down + remove_column :queries, :options + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1b/1b08ddf21a60c6f5d58afd83e4d47f0a83522899.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b08ddf21a60c6f5d58afd83e4d47f0a83522899.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1350 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 'forwardable' +require 'cgi' + +module ApplicationHelper + include Redmine::WikiFormatting::Macros::Definitions + include Redmine::I18n + include GravatarHelper::PublicMethods + include Redmine::Pagination::Helper + + extend Forwardable + def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter + + # Return true if user is authorized for controller/action, otherwise false + def authorize_for(controller, action) + User.current.allowed_to?({:controller => controller, :action => action}, @project) + end + + # Display a link if user is authorized + # + # @param [String] name Anchor text (passed to link_to) + # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized + # @param [optional, Hash] html_options Options passed to link_to + # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to + def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) + link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) + end + + # Displays a link to user's account page if active + def link_to_user(user, options={}) + if user.is_a?(User) + name = h(user.name(options[:format])) + if user.active? || (User.current.admin? && user.logged?) + link_to name, user_path(user), :class => user.css_classes + else + name + end + else + h(user.to_s) + end + end + + # Displays a link to +issue+ with its subject. + # Examples: + # + # link_to_issue(issue) # => Defect #6: This is the subject + # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... + # link_to_issue(issue, :subject => false) # => Defect #6 + # link_to_issue(issue, :project => true) # => Foo - Defect #6 + # link_to_issue(issue, :subject => false, :tracker => false) # => #6 + # + def link_to_issue(issue, options={}) + title = nil + subject = nil + text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" + if options[:subject] == false + title = issue.subject.truncate(60) + else + subject = issue.subject + if truncate_length = options[:truncate] + subject = subject.truncate(truncate_length) + end + end + only_path = options[:only_path].nil? ? true : options[:only_path] + s = link_to(text, issue_path(issue, :only_path => only_path), + :class => issue.css_classes, :title => title) + s << h(": #{subject}") if subject + s = h("#{issue.project} - ") + s if options[:project] + s + end + + # Generates a link to an attachment. + # Options: + # * :text - Link text (default to attachment filename) + # * :download - Force download (default: false) + def link_to_attachment(attachment, options={}) + text = options.delete(:text) || attachment.filename + route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path + html_options = options.slice!(:only_path) + url = send(route_method, attachment, attachment.filename, options) + link_to text, url, html_options + end + + # Generates a link to a SCM revision + # Options: + # * :text - Link text (default to the formatted revision) + def link_to_revision(revision, repository, options={}) + if repository.is_a?(Project) + repository = repository.repository + end + text = options.delete(:text) || format_revision(revision) + rev = revision.respond_to?(:identifier) ? revision.identifier : revision + link_to( + h(text), + {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision)) + ) + end + + # Generates a link to a message + def link_to_message(message, options={}, html_options = nil) + link_to( + message.subject.truncate(60), + board_message_path(message.board_id, message.parent_id || message.id, { + :r => (message.parent_id && message.id), + :anchor => (message.parent_id ? "message-#{message.id}" : nil) + }.merge(options)), + html_options + ) + end + + # Generates a link to a project if active + # Examples: + # + # link_to_project(project) # => link to the specified project overview + # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options + # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) + # + def link_to_project(project, options={}, html_options = nil) + if project.archived? + h(project.name) + elsif options.key?(:action) + ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0." + url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) + link_to project.name, url, html_options + else + link_to project.name, project_path(project, options), html_options + end + end + + # Generates a link to a project settings if active + def link_to_project_settings(project, options={}, html_options=nil) + if project.active? + link_to project.name, settings_project_path(project, options), html_options + elsif project.archived? + h(project.name) + else + link_to project.name, project_path(project, options), html_options + end + end + + # Helper that formats object for html or text rendering + def format_object(object, html=true) + case object.class.name + when 'Array' + object.map {|o| format_object(o, html)}.join(', ').html_safe + when 'Time' + format_time(object) + when 'Date' + format_date(object) + when 'Fixnum' + object.to_s + when 'Float' + sprintf "%.2f", object + when 'User' + html ? link_to_user(object) : object.to_s + when 'Project' + html ? link_to_project(object) : object.to_s + when 'Version' + html ? link_to(object.name, version_path(object)) : object.to_s + when 'TrueClass' + l(:general_text_Yes) + when 'FalseClass' + l(:general_text_No) + when 'Issue' + object.visible? && html ? link_to_issue(object) : "##{object.id}" + when 'CustomValue', 'CustomFieldValue' + if object.custom_field + f = object.custom_field.format.formatted_custom_value(self, object, html) + if f.nil? || f.is_a?(String) + f + else + format_object(f, html) + end + else + object.value.to_s + end + else + html ? h(object) : object.to_s + end + end + + def wiki_page_path(page, options={}) + url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) + end + + def thumbnail_tag(attachment) + link_to image_tag(thumbnail_path(attachment)), + named_attachment_path(attachment, attachment.filename), + :title => attachment.filename + end + + def toggle_link(name, id, options={}) + onclick = "$('##{id}').toggle(); " + onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") + onclick << "return false;" + link_to(name, "#", :onclick => onclick) + end + + def image_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + tag(:input, html_options.merge({ + :type => "image", :src => image_path(name), + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + def format_activity_title(text) + h(truncate_single_line_raw(text, 100)) + end + + def format_activity_day(date) + date == User.current.today ? l(:label_today).titleize : format_date(date) + end + + def format_activity_description(text) + h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') + ).gsub(/[\r\n]+/, "
    ").html_safe + end + + def format_version_name(version) + if version.project == @project + h(version) + else + h("#{version.project} - #{version}") + end + end + + def due_date_distance_in_words(date) + if date + l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) + end + end + + # Renders a tree of projects as a nested set of unordered lists + # The given collection may be a subset of the whole project tree + # (eg. some intermediate nodes are private and can not be seen) + def render_project_nested_lists(projects) + s = '' + if projects.any? + ancestors = [] + original_project = @project + projects.sort_by(&:lft).each do |project| + # set the project environment to please macros. + @project = project + if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) + s << "
      \n" + else + ancestors.pop + s << "" + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + s << "
    \n" + end + end + classes = (ancestors.empty? ? 'root' : 'child') + s << "
  • " + s << h(block_given? ? yield(project) : project.name) + s << "
    \n" + ancestors << project + end + s << ("
  • \n" * ancestors.size) + @project = original_project + end + s.html_safe + end + + def render_page_hierarchy(pages, node=nil, options={}) + content = '' + if pages[node] + content << "
      \n" + pages[node].each do |page| + content << "
    • " + content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}, + :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) + content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] + content << "
    • \n" + end + content << "
    \n" + end + content.html_safe + end + + # Renders flash messages + def render_flash_messages + s = '' + flash.each do |k,v| + s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") + end + s.html_safe + end + + # Renders tabs and their content + def render_tabs(tabs, selected=params[:tab]) + if tabs.any? + unless tabs.detect {|tab| tab[:name] == selected} + selected = nil + end + selected ||= tabs.first[:name] + render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected} + else + content_tag 'p', l(:label_no_data), :class => "nodata" + end + end + + # Renders the project quick-jump box + def render_project_jump_box + return unless User.current.logged? + projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq + if projects.any? + options = + ("" + + '').html_safe + + options << project_tree_options_for_select(projects, :selected => @project) do |p| + { :value => project_path(:id => p, :jump => current_menu_item) } + end + + select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') + end + end + + def project_tree_options_for_select(projects, options = {}) + s = '' + project_tree(projects) do |project, level| + name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe + tag_options = {:value => project.id} + if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) + tag_options[:selected] = 'selected' + else + tag_options[:selected] = nil + end + tag_options.merge!(yield(project)) if block_given? + s << content_tag('option', name_prefix + h(project), tag_options) + end + s.html_safe + end + + # Yields the given block for each project with its level in the tree + # + # Wrapper for Project#project_tree + def project_tree(projects, &block) + Project.project_tree(projects, &block) + end + + def principals_check_box_tags(name, principals) + s = '' + principals.each do |principal| + s << "\n" + end + s.html_safe + end + + # Returns a string for users/groups option tags + def principals_options_for_select(collection, selected=nil) + s = '' + if collection.include?(User.current) + s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) + end + groups = '' + collection.sort.each do |element| + selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected + (element.is_a?(Group) ? groups : s) << %() + end + unless groups.empty? + s << %(#{groups}) + end + s.html_safe + end + + # Options for the new membership projects combo-box + def options_for_membership_project_select(principal, projects) + options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options << project_tree_options_for_select(projects) do |p| + {:disabled => principal.projects.to_a.include?(p)} + end + options + end + + def option_tag(name, text, value, selected=nil, options={}) + content_tag 'option', value, options.merge(:value => value, :selected => (value == selected)) + end + + # Truncates and returns the string as a single line + def truncate_single_line(string, *args) + ActiveSupport::Deprecation.warn( + "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring") + # Rails 4 ActionView::Helpers::TextHelper#truncate escapes. + # So, result is broken. + truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') + end + + def truncate_single_line_raw(string, length) + string.truncate(length).gsub(%r{[\r\n]+}m, ' ') + end + + # Truncates at line break after 250 characters or options[:length] + def truncate_lines(string, options={}) + length = options[:length] || 250 + if string.to_s =~ /\A(.{#{length}}.*?)$/m + "#{$1}..." + else + string + end + end + + def anchor(text) + text.to_s.gsub(' ', '_') + end + + def html_hours(text) + text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe + end + + def authoring(created, author, options={}) + l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe + end + + def time_tag(time) + text = distance_of_time_in_words(Time.now, time) + if @project + link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) + else + content_tag('abbr', text, :title => format_time(time)) + end + end + + def syntax_highlight_lines(name, content) + lines = [] + syntax_highlight(name, content).each_line { |line| lines << line } + lines + end + + def syntax_highlight(name, content) + Redmine::SyntaxHighlighting.highlight_by_filename(content, name) + end + + def to_path_param(path) + str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") + str.blank? ? nil : str + end + + def reorder_links(name, url, method = :post) + link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), + url.merge({"#{name}[move_to]" => 'highest'}), + :method => method, :title => l(:label_sort_highest)) + + link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), + url.merge({"#{name}[move_to]" => 'higher'}), + :method => method, :title => l(:label_sort_higher)) + + link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), + url.merge({"#{name}[move_to]" => 'lower'}), + :method => method, :title => l(:label_sort_lower)) + + link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), + url.merge({"#{name}[move_to]" => 'lowest'}), + :method => method, :title => l(:label_sort_lowest)) + end + + def breadcrumb(*args) + elements = args.flatten + elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil + end + + def other_formats_links(&block) + concat('

    '.html_safe + l(:label_export_to)) + yield Redmine::Views::OtherFormatsBuilder.new(self) + concat('

    '.html_safe) + end + + def page_header_title + if @project.nil? || @project.new_record? + h(Setting.app_title) + else + b = [] + ancestors = (@project.root? ? [] : @project.ancestors.visible.all) + if ancestors.any? + root = ancestors.shift + b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') + if ancestors.size > 2 + b << "\xe2\x80\xa6" + ancestors = ancestors[-2, 2] + end + b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } + end + b << h(@project) + b.join(" \xc2\xbb ").html_safe + end + end + + # Returns a h2 tag and sets the html title with the given arguments + def title(*args) + strings = args.map do |arg| + if arg.is_a?(Array) && arg.size >= 2 + link_to(*arg) + else + h(arg.to_s) + end + end + html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s} + content_tag('h2', strings.join(' » ').html_safe) + end + + # Sets the html title + # Returns the html title when called without arguments + # Current project name and app_title and automatically appended + # Exemples: + # html_title 'Foo', 'Bar' + # html_title # => 'Foo - Bar - My Project - Redmine' + def html_title(*args) + if args.empty? + title = @html_title || [] + title << @project.name if @project + title << Setting.app_title unless Setting.app_title == title.last + title.reject(&:blank?).join(' - ') + else + @html_title ||= [] + @html_title += args + end + end + + # Returns the theme, controller name, and action as css classes for the + # HTML body. + def body_css_classes + css = [] + if theme = Redmine::Themes.theme(Setting.ui_theme) + css << 'theme-' + theme.name + end + + css << 'project-' + @project.identifier if @project && @project.identifier.present? + css << 'controller-' + controller_name + css << 'action-' + action_name + css.join(' ') + end + + def accesskey(s) + @used_accesskeys ||= [] + key = Redmine::AccessKeys.key_for(s) + return nil if @used_accesskeys.include?(key) + @used_accesskeys << key + key + end + + # Formats text according to system settings. + # 2 ways to call this method: + # * with a String: textilizable(text, options) + # * with an object and one of its attribute: textilizable(issue, :description, options) + def textilizable(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + case args.size + when 1 + obj = options[:object] + text = args.shift + when 2 + obj = args.shift + attr = args.shift + text = obj.send(attr).to_s + else + raise ArgumentError, 'invalid arguments to textilizable' + end + return '' if text.blank? + project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) + only_path = options.delete(:only_path) == false ? false : true + + text = text.dup + macros = catch_macros(text) + text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) + + @parsed_headings = [] + @heading_anchors = {} + @current_section = 0 if options[:edit_section_links] + + parse_sections(text, project, obj, attr, only_path, options) + text = parse_non_pre_blocks(text, obj, macros) do |text| + [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| + send method_name, text, project, obj, attr, only_path, options + end + end + parse_headings(text, project, obj, attr, only_path, options) + + if @parsed_headings.any? + replace_toc(text, @parsed_headings) + end + + text.html_safe + end + + def parse_non_pre_blocks(text, obj, macros) + s = StringScanner.new(text) + tags = [] + parsed = '' + while !s.eos? + s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) + text, full_tag, closing, tag = s[1], s[2], s[3], s[4] + if tags.empty? + yield text + inject_macros(text, obj, macros) if macros.any? + else + inject_macros(text, obj, macros, false) if macros.any? + end + parsed << text + if tag + if closing + if tags.last == tag.downcase + tags.pop + end + else + tags << tag.downcase + end + parsed << full_tag + end + end + # Close any non closing tags + while tag = tags.pop + parsed << "" + end + parsed + end + + def parse_inline_attachments(text, project, obj, attr, only_path, options) + # when using an image link, try to use an attachment, if possible + attachments = options[:attachments] || [] + attachments += obj.attachments if obj.respond_to?(:attachments) + if attachments.present? + text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| + filename, ext, alt, alttext = $1.downcase, $2, $3, $4 + # search for the picture in attachments + if found = Attachment.latest_attach(attachments, filename) + image_url = download_named_attachment_path(found, found.filename, :only_path => only_path) + desc = found.description.to_s.gsub('"', '') + if !desc.blank? && alttext.blank? + alt = " title=\"#{desc}\" alt=\"#{desc}\"" + end + "src=\"#{image_url}\"#{alt}" + else + m + end + end + end + end + + # Wiki links + # + # Examples: + # [[mypage]] + # [[mypage|mytext]] + # wiki links can refer other project wikis, using project name or identifier: + # [[project:]] -> wiki starting page + # [[project:|mytext]] + # [[project:mypage]] + # [[project:mypage|mytext]] + def parse_wiki_links(text, project, obj, attr, only_path, options) + text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| + link_project = project + esc, all, page, title = $1, $2, $3, $5 + if esc.nil? + if page =~ /^([^\:]+)\:(.*)$/ + identifier, page = $1, $2 + link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier) + title ||= identifier if page.blank? + end + + if link_project && link_project.wiki + # extract anchor + anchor = nil + if page =~ /^(.+?)\#(.+)$/ + page, anchor = $1, $2 + end + anchor = sanitize_anchor_name(anchor) if anchor.present? + # check if page exists + wiki_page = link_project.wiki.find_page(page) + url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page + "##{anchor}" + else + case options[:wiki_links] + when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') + when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export + else + wiki_page_id = page.present? ? Wiki.titleize(page) : nil + parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil + url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, + :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) + end + end + link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + all + end + else + all + end + end + end + + # Redmine links + # + # Examples: + # Issues: + # #52 -> Link to issue #52 + # Changesets: + # r52 -> Link to revision 52 + # commit:a85130f -> Link to scmid starting with a85130f + # Documents: + # document#17 -> Link to document with id 17 + # document:Greetings -> Link to the document with title "Greetings" + # document:"Some document" -> Link to the document with title "Some document" + # Versions: + # version#3 -> Link to version with id 3 + # version:1.0.0 -> Link to version named "1.0.0" + # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" + # Attachments: + # attachment:file.zip -> Link to the attachment of the current object named file.zip + # Source files: + # source:some/file -> Link to the file located at /some/file in the project's repository + # source:some/file@52 -> Link to the file's revision 52 + # source:some/file#L120 -> Link to line 120 of the file + # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 + # export:some/file -> Force the download of the file + # Forum messages: + # message#1218 -> Link to message with id 1218 + # Projects: + # project:someproject -> Link to project named "someproject" + # project#3 -> Link to project with id 3 + # + # Links can refer other objects from other projects, using project identifier: + # identifier:r52 + # identifier:document:"Some document" + # identifier:version:1.0.0 + # identifier:source:some/file + def parse_redmine_links(text, default_project, obj, attr, only_path, options) + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| + leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 + link = nil + project = default_project + if project_identifier + project = Project.visible.find_by_identifier(project_identifier) + end + if esc.nil? + if prefix.nil? && sep == 'r' + if project + repository = nil + if repo_identifier + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + # project.changesets.visible raises an SQL error because of a double join on repositories + if repository && + (changeset = Changeset.visible. + find_by_repository_id_and_revision(repository.id, identifier)) + link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), + {:only_path => only_path, :controller => 'repositories', + :action => 'revision', :id => project, + :repository_id => repository.identifier_param, + :rev => changeset.revision}, + :class => 'changeset', + :title => truncate_single_line_raw(changeset.comments, 100)) + end + end + elsif sep == '#' + oid = identifier.to_i + case prefix + when nil + if oid.to_s == identifier && + issue = Issue.visible.includes(:status).find_by_id(oid) + anchor = comment_id ? "note-#{comment_id}" : nil + link = link_to(h("##{oid}#{comment_suffix}"), + {:only_path => only_path, :controller => 'issues', + :action => 'show', :id => oid, :anchor => anchor}, + :class => issue.css_classes, + :title => "#{issue.subject.truncate(100)} (#{issue.status.name})") + end + when 'document' + if document = Document.visible.find_by_id(oid) + link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, + :class => 'document' + end + when 'version' + if version = Version.visible.find_by_id(oid) + link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'message' + if message = Message.visible.includes(:parent).find_by_id(oid) + link = link_to_message(message, {:only_path => only_path}, :class => 'message') + end + when 'forum' + if board = Board.visible.find_by_id(oid) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if news = News.visible.find_by_id(oid) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end + when 'project' + if p = Project.visible.find_by_id(oid) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + elsif sep == ':' + # removes the double quotes if any + name = identifier.gsub(%r{^"(.*)"$}, "\\1") + name = CGI.unescapeHTML(name) + case prefix + when 'document' + if project && document = project.documents.visible.find_by_title(name) + link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, + :class => 'document' + end + when 'version' + if project && version = project.versions.visible.find_by_name(name) + link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'forum' + if project && board = project.boards.visible.find_by_name(name) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if project && news = project.news.visible.find_by_title(name) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end + when 'commit', 'source', 'export' + if project + repository = nil + if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$} + repo_prefix, repo_identifier, name = $1, $2, $3 + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + if prefix == 'commit' + if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first) + link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, + :class => 'changeset', + :title => truncate_single_line_raw(changeset.comments, 100) + end + else + if repository && User.current.allowed_to?(:browse_repository, project) + name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$} + path, rev, anchor = $1, $3, $5 + link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, + :path => to_path_param(path), + :rev => rev, + :anchor => anchor}, + :class => (prefix == 'export' ? 'source download' : 'source') + end + end + repo_prefix = nil + end + when 'attachment' + attachments = options[:attachments] || [] + attachments += obj.attachments if obj.respond_to?(:attachments) + if attachments && attachment = Attachment.latest_attach(attachments, name) + link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment') + end + when 'project' + if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + end + end + (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) + end + end + + HEADING_RE = /(]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) + + def parse_sections(text, project, obj, attr, only_path, options) + return unless options[:edit_section_links] + text.gsub!(HEADING_RE) do + heading = $1 + @current_section += 1 + if @current_section > 1 + content_tag('div', + link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), + :class => 'contextual', + :title => l(:button_edit_section), + :id => "section-#{@current_section}") + heading.html_safe + else + heading + end + end + end + + # Headings and TOC + # Adds ids and links to headings unless options[:headings] is set to false + def parse_headings(text, project, obj, attr, only_path, options) + return if options[:headings] == false + + text.gsub!(HEADING_RE) do + level, attrs, content = $2.to_i, $3, $4 + item = strip_tags(content).strip + anchor = sanitize_anchor_name(item) + # used for single-file wiki export + anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) + @heading_anchors[anchor] ||= 0 + idx = (@heading_anchors[anchor] += 1) + if idx > 1 + anchor = "#{anchor}-#{idx}" + end + @parsed_headings << [level, anchor, item] + "\n#{content}" + end + end + + MACROS_RE = /( + (!)? # escaping + ( + \{\{ # opening tag + ([\w]+) # macro name + (\(([^\n\r]*?)\))? # optional arguments + ([\n\r].*?[\n\r])? # optional block of text + \}\} # closing tag + ) + )/mx unless const_defined?(:MACROS_RE) + + MACRO_SUB_RE = /( + \{\{ + macro\((\d+)\) + \}\} + )/x unless const_defined?(:MACRO_SUB_RE) + + # Extracts macros from text + def catch_macros(text) + macros = {} + text.gsub!(MACROS_RE) do + all, macro = $1, $4.downcase + if macro_exists?(macro) || all =~ MACRO_SUB_RE + index = macros.size + macros[index] = all + "{{macro(#{index})}}" + else + all + end + end + macros + end + + # Executes and replaces macros in text + def inject_macros(text, obj, macros, execute=true) + text.gsub!(MACRO_SUB_RE) do + all, index = $1, $2.to_i + orig = macros.delete(index) + if execute && orig && orig =~ MACROS_RE + esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) + if esc.nil? + h(exec_macro(macro, obj, args, block) || all) + else + h(all) + end + elsif orig + h(orig) + else + h(all) + end + end + end + + TOC_RE = /

    \{\{((<|<)|(>|>))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE) + + # Renders the TOC with given headings + def replace_toc(text, headings) + text.gsub!(TOC_RE) do + left_align, right_align = $2, $3 + # Keep only the 4 first levels + headings = headings.select{|level, anchor, item| level <= 4} + if headings.empty? + '' + else + div_class = 'toc' + div_class << ' right' if right_align + div_class << ' left' if left_align + out = "

    • " + root = headings.map(&:first).min + current = root + started = false + headings.each do |level, anchor, item| + if level > current + out << '
      • ' * (level - current) + elsif level < current + out << "
      \n" * (current - level) + "
    • " + elsif started + out << '
    • ' + end + out << "#{item}" + current = level + started = true + end + out << '
    ' * (current - root) + out << '' + end + end + end + + # Same as Rails' simple_format helper without using paragraphs + def simple_format_without_paragraph(text) + text.to_s. + gsub(/\r\n?/, "\n"). # \r\n and \r -> \n + gsub(/\n\n+/, "

    "). # 2+ newline -> 2 br + gsub(/([^\n]\n)(?=[^\n])/, '\1
    '). # 1 newline -> br + html_safe + end + + def lang_options_for_select(blank=true) + (blank ? [["(auto)", ""]] : []) + languages_options + end + + def label_tag_for(name, option_tags = nil, options = {}) + label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") + content_tag("label", label_text) + end + + def labelled_form_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + if args.first.is_a?(Symbol) + options.merge!(:as => args.shift) + end + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + form_for(*args, &proc) + end + + def labelled_fields_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + fields_for(*args, &proc) + end + + def labelled_remote_form_for(*args, &proc) + ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) + form_for(*args, &proc) + end + + def error_messages_for(*objects) + html = "" + objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact + errors = objects.map {|o| o.errors.full_messages}.flatten + if errors.any? + html << "
      \n" + errors.each do |error| + html << "
    • #{h error}
    • \n" + end + html << "
    \n" + end + html.html_safe + end + + def delete_link(url, options={}) + options = { + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del' + }.merge(options) + + link_to l(:button_delete), url, options + end + + def preview_link(url, form, target='preview', options={}) + content_tag 'a', l(:label_preview), { + :href => "#", + :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, + :accesskey => accesskey(:preview) + }.merge(options) + end + + def link_to_function(name, function, html_options={}) + content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) + end + + # Helper to render JSON in views + def raw_json(arg) + arg.to_json.to_s.gsub('/', '\/').html_safe + end + + def back_url + url = params[:back_url] + if url.nil? && referer = request.env['HTTP_REFERER'] + url = CGI.unescape(referer.to_s) + end + url + end + + def back_url_hidden_field_tag + url = back_url + hidden_field_tag('back_url', url, :id => nil) unless url.blank? + end + + def check_all_links(form_name) + link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + + " | ".html_safe + + link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") + end + + def progress_bar(pcts, options={}) + pcts = [pcts, pcts] unless pcts.is_a?(Array) + pcts = pcts.collect(&:round) + pcts[1] = pcts[1] - pcts[0] + pcts << (100 - pcts[1] - pcts[0]) + width = options[:width] || '100px;' + legend = options[:legend] || '' + content_tag('table', + content_tag('tr', + (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + + (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + + (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) + ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe + + content_tag('p', legend, :class => 'percent').html_safe + end + + def checked_image(checked=true) + if checked + image_tag 'toggle_check.png' + end + end + + def context_menu(url) + unless @context_menu_included + content_for :header_tags do + javascript_include_tag('context_menu') + + stylesheet_link_tag('context_menu') + end + if l(:direction) == 'rtl' + content_for :header_tags do + stylesheet_link_tag('context_menu_rtl') + end + end + @context_menu_included = true + end + javascript_tag "contextMenuInit('#{ url_for(url) }')" + end + + def calendar_for(field_id) + include_calendar_headers_tags + javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") + end + + def include_calendar_headers_tags + unless @calendar_headers_tags_included + tags = javascript_include_tag("datepicker") + @calendar_headers_tags_included = true + content_for :header_tags do + start_of_week = Setting.start_of_week + start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? + # Redmine uses 1..7 (monday..sunday) in settings and locales + # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 + start_of_week = start_of_week.to_i % 7 + tags << javascript_tag( + "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + + "showOn: 'button', buttonImageOnly: true, buttonImage: '" + + path_to_image('/images/calendar.png') + + "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " + + "selectOtherMonths: true, changeMonth: true, changeYear: true, " + + "beforeShow: beforeShowDatePicker};") + jquery_locale = l('jquery.locale', :default => current_language.to_s) + unless jquery_locale == 'en' + tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") + end + tags + end + end + end + + # Overrides Rails' stylesheet_link_tag with themes and plugins support. + # Examples: + # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults + # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets + # + def stylesheet_link_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/stylesheets/#{source}" + elsif current_theme && current_theme.stylesheets.include?(source) + current_theme.stylesheet_path(source) + else + source + end + end + super sources, options + end + + # Overrides Rails' image_tag with themes and plugins support. + # Examples: + # image_tag('image.png') # => picks image.png from the current theme or defaults + # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets + # + def image_tag(source, options={}) + if plugin = options.delete(:plugin) + source = "/plugin_assets/#{plugin}/images/#{source}" + elsif current_theme && current_theme.images.include?(source) + source = current_theme.image_path(source) + end + super source, options + end + + # Overrides Rails' javascript_include_tag with plugins support + # Examples: + # javascript_include_tag('scripts') # => picks scripts.js from defaults + # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets + # + def javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + if plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/javascripts/#{source}" + else + source + end + end + end + super sources, options + end + + # TODO: remove this in 2.5.0 + def has_content?(name) + content_for?(name) + end + + def sidebar_content? + content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present? + end + + def view_layouts_base_sidebar_hook_response + @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) + end + + def email_delivery_enabled? + !!ActionMailer::Base.perform_deliveries + end + + # Returns the avatar image tag for the given +user+ if avatars are enabled + # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') + def avatar(user, options = { }) + if Setting.gravatar_enabled? + options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) + email = nil + if user.respond_to?(:mail) + email = user.mail + elsif user.to_s =~ %r{<(.+?)>} + email = $1 + end + return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil + else + '' + end + end + + def sanitize_anchor_name(anchor) + if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' + anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + else + # TODO: remove when ruby1.8 is no longer supported + anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + end + end + + # Returns the javascript tags that are included in the html layout head + def javascript_heads + tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application') + unless User.current.pref.warn_on_leaving_unsaved == '0' + tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") + end + tags + end + + def favicon + "".html_safe + end + + # Returns the path to the favicon + def favicon_path + icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico' + image_path(icon) + end + + # Returns the full URL to the favicon + def favicon_url + # TODO: use #image_url introduced in Rails4 + path = favicon_path + base = url_for(:controller => 'welcome', :action => 'index', :only_path => false) + base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'') + end + + def robot_exclusion_tag + ''.html_safe + end + + # Returns true if arg is expected in the API response + def include_in_api_response?(arg) + unless @included_in_api_response + param = params[:include] + @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') + @included_in_api_response.collect!(&:strip) + end + @included_in_api_response.include?(arg.to_s) + end + + # Returns options or nil if nometa param or X-Redmine-Nometa header + # was set in the request + def api_meta(options) + if params[:nometa].present? || request.headers['X-Redmine-Nometa'] + # compatibility mode for activeresource clients that raise + # an error when unserializing an array with attributes + nil + else + options + end + end + + private + + def wiki_helper + helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) + extend helper + return self + end + + def link_to_content_update(text, url_params = {}, html_options = {}) + link_to(text, url_params, html_options) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1b/1b1f0da73986061f36bbd7cd1895fc7ac1182885.svn-base --- a/.svn/pristine/1b/1b1f0da73986061f36bbd7cd1895fc7ac1182885.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,140 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 MessagesController < ApplicationController - menu_item :boards - default_search_scope :messages - before_filter :find_board, :only => [:new, :preview] - before_filter :find_message, :except => [:new, :preview] - before_filter :authorize, :except => [:preview, :edit, :destroy] - - helper :boards - helper :watchers - helper :attachments - include AttachmentsHelper - - REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE) - - # Show a topic and its replies - def show - page = params[:page] - # Find the page of the requested reply - if params[:r] && page.nil? - offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i]) - page = 1 + offset / REPLIES_PER_PAGE - end - - @reply_count = @topic.children.count - @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page - @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}], - :order => "#{Message.table_name}.created_on ASC", - :limit => @reply_pages.items_per_page, - :offset => @reply_pages.current.offset) - - @reply = Message.new(:subject => "RE: #{@message.subject}") - render :action => "show", :layout => false if request.xhr? - end - - # Create a new topic - def new - @message = Message.new - @message.author = User.current - @message.board = @board - @message.safe_attributes = params[:message] - if request.post? - @message.save_attachments(params[:attachments]) - if @message.save - call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) - render_attachment_warning_if_needed(@message) - redirect_to board_message_path(@board, @message) - end - end - end - - # Reply to a topic - def reply - @reply = Message.new - @reply.author = User.current - @reply.board = @board - @reply.safe_attributes = params[:reply] - @topic.children << @reply - if !@reply.new_record? - call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply}) - attachments = Attachment.attach_files(@reply, params[:attachments]) - render_attachment_warning_if_needed(@reply) - end - redirect_to board_message_path(@board, @topic, :r => @reply) - end - - # Edit a message - def edit - (render_403; return false) unless @message.editable_by?(User.current) - @message.safe_attributes = params[:message] - if request.post? && @message.save - attachments = Attachment.attach_files(@message, params[:attachments]) - render_attachment_warning_if_needed(@message) - flash[:notice] = l(:notice_successful_update) - @message.reload - redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id)) - end - end - - # Delete a messages - def destroy - (render_403; return false) unless @message.destroyable_by?(User.current) - r = @message.to_param - @message.destroy - if @message.parent - redirect_to board_message_path(@board, @message.parent, :r => r) - else - redirect_to project_board_path(@project, @board) - end - end - - def quote - @subject = @message.subject - @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:') - - @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> " - @content << @message.content.to_s.strip.gsub(%r{
    ((.|\s)*?)
    }m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" - end - - def preview - message = @board.messages.find_by_id(params[:id]) - @attachements = message.attachments if message - @text = (params[:message] || params[:reply])[:content] - @previewed = message - render :partial => 'common/preview' - end - -private - def find_message - return unless find_board - @message = @board.messages.find(params[:id], :include => :parent) - @topic = @message.root - rescue ActiveRecord::RecordNotFound - render_404 - end - - def find_board - @board = Board.find(params[:board_id], :include => :project) - @project = @board.project - rescue ActiveRecord::RecordNotFound - render_404 - nil - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1b/1b2837ef400afd5bc210e52e7203d86ba25be2b4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b2837ef400afd5bc210e52e7203d86ba25be2b4.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,12 @@ +<%= "(#{l(:field_private_notes)}) " if @journal.private_notes? -%><%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %> + +<% details_to_strings(@journal_details, true).each do |string| -%> +<%= string %> +<% end -%> + +<% if @journal.notes? -%> +<%= @journal.notes %> + +<% end -%> +---------------------------------------- +<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :users => @users, :issue_url => @issue_url } %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1b/1b29dadeac443afcb6fd27a4a9c92482c01b0a1c.svn-base --- a/.svn/pristine/1b/1b29dadeac443afcb6fd27a4a9c92482c01b0a1c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1084 +0,0 @@ -# Redmine EU language -# Author: Ales Zabala Alava (Shagi), -# 2010-01-25 -# Distributed under the same terms as the Redmine itself. -eu: - direction: ltr - 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: [Igandea, Astelehena, Asteartea, Asteazkena, Osteguna, Ostirala, Larunbata] - abbr_day_names: [Ig., Al., Ar., Az., Og., Or., La.] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, Urtarrila, Otsaila, Martxoa, Apirila, Maiatza, Ekaina, Uztaila, Abuztua, Iraila, Urria, Azaroa, Abendua] - abbr_month_names: [~, Urt, Ots, Mar, Api, Mai, Eka, Uzt, Abu, Ira, Urr, Aza, Abe] - # Used in date_select and datime_select. - order: - - :year - - :month - - :day - - time: - formats: - default: "%Y/%m/%d %H:%M" - time: "%H:%M" - short: "%b %d %H:%M" - long: "%Y %B %d %H:%M" - am: "am" - pm: "pm" - - datetime: - distance_in_words: - half_a_minute: "minutu erdi" - less_than_x_seconds: - one: "segundu bat baino gutxiago" - other: "%{count} segundu baino gutxiago" - x_seconds: - one: "segundu 1" - other: "%{count} segundu" - less_than_x_minutes: - one: "minutu bat baino gutxiago" - other: "%{count} minutu baino gutxiago" - x_minutes: - one: "minutu 1" - other: "%{count} minutu" - about_x_hours: - one: "ordu 1 inguru" - other: "%{count} ordu inguru" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "egun 1" - other: "%{count} egun" - about_x_months: - one: "hilabete 1 inguru" - other: "%{count} hilabete inguru" - x_months: - one: "hilabete 1" - other: "%{count} hilabete" - about_x_years: - one: "urte 1 inguru" - other: "%{count} urte inguru" - over_x_years: - one: "urte 1 baino gehiago" - other: "%{count} urte baino gehiago" - almost_x_years: - one: "ia urte 1" - other: "ia %{count} urte" - - number: - format: - separator: "." - delimiter: "" - precision: 3 - human: - format: - delimiter: "" - precision: 3 - storage_units: - format: "%n %u" - units: - byte: - one: "Byte" - other: "Byte" - kb: "KB" - mb: "MB" - gb: "GB" - tb: "TB" - -# Used in array.to_sentence. - support: - array: - sentence_connector: "eta" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "Errore batek %{model} hau godetzea galarazi du." - other: "%{count} errorek %{model} hau gordetzea galarazi dute." - messages: - inclusion: "ez dago zerrendan" - exclusion: "erreserbatuta dago" - invalid: "baliogabea da" - confirmation: "ez du berrespenarekin bat egiten" - accepted: "onartu behar da" - empty: "ezin da hutsik egon" - blank: "ezin da hutsik egon" - too_long: "luzeegia da (maximoa %{count} karaktere dira)" - too_short: "laburregia da (minimoa %{count} karaktere dira)" - wrong_length: "luzera ezegokia da (%{count} karakter izan beharko litzake)" - taken: "dagoeneko hartuta dago" - not_a_number: "ez da zenbaki bat" - not_a_date: "ez da baliozko data" - greater_than: "%{count} baino handiagoa izan behar du" - greater_than_or_equal_to: "%{count} edo handiagoa izan behar du" - equal_to: "%{count} izan behar du" - less_than: "%{count} baino gutxiago izan behar du" - less_than_or_equal_to: "%{count} edo gutxiago izan behar du" - odd: "bakoitia izan behar du" - even: "bikoitia izan behar du" - greater_than_start_date: "hasiera data baino handiagoa izan behar du" - not_same_project: "ez dago proiektu berdinean" - circular_dependency: "Erlazio honek mendekotasun zirkular bat sortuko luke" - cant_link_an_issue_with_a_descendant: "Zeregin bat ezin da bere azpiataza batekin estekatu." - - actionview_instancetag_blank_option: Hautatu mesedez - - general_text_No: 'Ez' - general_text_Yes: 'Bai' - general_text_no: 'ez' - general_text_yes: 'bai' - general_lang_name: 'Euskara' - general_csv_separator: ',' - general_csv_decimal_separator: '.' - general_csv_encoding: UTF-8 - general_pdf_encoding: UTF-8 - general_first_day_of_week: '1' - - notice_account_updated: Kontua ongi eguneratu da. - notice_account_invalid_creditentials: Erabiltzaile edo pasahitz ezegokia - notice_account_password_updated: Pasahitza ongi eguneratu da. - notice_account_wrong_password: Pasahitz ezegokia. - notice_account_register_done: Kontua ongi sortu da. Kontua gaitzeko klikatu epostan adierazi zaizun estekan. - notice_account_unknown_email: Erabiltzaile ezezaguna. - notice_can_t_change_password: Kontu honek kanpoko autentikazio bat erabiltzen du. Ezinezkoa da pasahitza aldatzea. - notice_account_lost_email_sent: Pasahitz berria aukeratzeko jarraibideak dituen eposta bat bidali zaizu. - notice_account_activated: Zure kontua gaituta dago. Orain saioa has dezakezu - notice_successful_create: Sortze arrakastatsua. - notice_successful_update: Eguneratze arrakastatsua. - notice_successful_delete: Ezabaketa arrakastatsua. - notice_successful_connection: Konexio arrakastatsua. - notice_file_not_found: Atzitu nahi duzun orria ez da exisitzen edo ezabatua izan da. - notice_locking_conflict: Beste erabiltzaile batek datuak eguneratu ditu. - notice_not_authorized: Ez duzu orri hau atzitzeko baimenik. - notice_email_sent: "%{value} helbidera eposta bat bidali da" - notice_email_error: "Errorea eposta bidaltzean (%{value})" - notice_feeds_access_key_reseted: Zure RSS atzipen giltza berrezarri da. - notice_api_access_key_reseted: Zure API atzipen giltza berrezarri da. - notice_failed_to_save_issues: "Hautatutako %{total} zereginetatik %{count} ezin izan dira konpondu: %{ids}." - notice_no_issue_selected: "Ez da zereginik hautatu! Mesedez, editatu nahi dituzun arazoak markatu." - notice_account_pending: "Zure kontua sortu da, orain kudeatzailearen onarpenaren zain dago." - notice_default_data_loaded: Lehenetsitako konfigurazioa ongi kargatu da. - notice_unable_delete_version: Ezin da bertsioa ezabatu. - notice_issue_done_ratios_updated: Burututako zereginen erlazioa eguneratu da. - - error_can_t_load_default_data: "Ezin izan da lehenetsitako konfigurazioa kargatu: %{value}" - error_scm_not_found: "Sarrera edo berrikuspena ez da biltegian topatu." - error_scm_command_failed: "Errorea gertatu da biltegia atzitzean: %{value}" - error_scm_annotate: "Sarrera ez da existitzen edo ezin da anotatu." - error_issue_not_found_in_project: 'Zeregina ez da topatu edo ez da proiektu honetakoa' - error_no_tracker_in_project: 'Proiektu honek ez du aztarnaririk esleituta. Mesedez egiaztatu Proiektuaren ezarpenak.' - error_no_default_issue_status: 'Zereginek ez dute lehenetsitako egoerarik. Mesedez egiaztatu zure konfigurazioa ("Kudeaketa -> Arazoen egoerak" atalera joan).' - error_can_not_reopen_issue_on_closed_version: 'Itxitako bertsio batera esleitutako zereginak ezin dira berrireki' - error_can_not_archive_project: Proiektu hau ezin da artxibatu - error_issue_done_ratios_not_updated: "Burututako zereginen erlazioa ez da eguneratu." - error_workflow_copy_source: 'Mesedez hautatu iturburuko aztarnari edo rola' - error_workflow_copy_target: 'Mesedez hautatu helburuko aztarnari(ak) edo rola(k)' - - warning_attachments_not_saved: "%{count} fitxategi ezin izan d(ir)a gorde." - - mail_subject_lost_password: "Zure %{value} pasahitza" - mail_body_lost_password: 'Zure pasahitza aldatzeko hurrengo estekan klikatu:' - mail_subject_register: "Zure %{value} kontuaren gaitzea" - mail_body_register: 'Zure kontua gaitzeko hurrengo estekan klikatu:' - mail_body_account_information_external: "Zure %{value} kontua erabil dezakezu saioa hasteko." - mail_body_account_information: Zure kontuaren informazioa - mail_subject_account_activation_request: "%{value} kontu gaitzeko eskaera" - mail_body_account_activation_request: "Erabiltzaile berri bat (%{value}) erregistratu da. Kontua zure onarpenaren zain dago:" - mail_subject_reminder: "%{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a" - mail_body_reminder: "Zuri esleituta dauden %{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a:" - mail_subject_wiki_content_added: "'%{id}' wiki orria gehitu da" - mail_body_wiki_content_added: "%{author}-(e)k '%{id}' wiki orria gehitu du." - mail_subject_wiki_content_updated: "'%{id}' wiki orria eguneratu da" - mail_body_wiki_content_updated: "%{author}-(e)k '%{id}' wiki orria eguneratu du." - - gui_validation_error: akats 1 - gui_validation_error_plural: "%{count} akats" - - field_name: Izena - field_description: Deskribapena - field_summary: Laburpena - field_is_required: Beharrezkoa - field_firstname: Izena - field_lastname: Abizenak - field_mail: Eposta - field_filename: Fitxategia - field_filesize: Tamaina - field_downloads: Deskargak - field_author: Egilea - field_created_on: Sortuta - field_updated_on: Eguneratuta - field_field_format: Formatua - field_is_for_all: Proiektu guztietarako - field_possible_values: Balio posibleak - field_regexp: Expresio erregularra - field_min_length: Luzera minimoa - field_max_length: Luzera maxioma - field_value: Balioa - field_category: Kategoria - field_title: Izenburua - field_project: Proiektua - field_issue: Zeregina - field_status: Egoera - field_notes: Oharrak - field_is_closed: Itxitako arazoa - field_is_default: Lehenetsitako balioa - field_tracker: Aztarnaria - field_subject: Gaia - field_due_date: Amaiera data - field_assigned_to: Esleituta - field_priority: Lehentasuna - field_fixed_version: Helburuko bertsioa - field_user: Erabiltzilea - field_role: Rola - field_homepage: Orri nagusia - field_is_public: Publikoa - field_parent: "Honen azpiproiektua:" - field_is_in_roadmap: Arazoak ibilbide-mapan erakutsi - field_login: Erabiltzaile izena - field_mail_notification: Eposta jakinarazpenak - field_admin: Kudeatzailea - field_last_login_on: Azken konexioa - field_language: Hizkuntza - field_effective_date: Data - field_password: Pasahitza - field_new_password: Pasahitz berria - field_password_confirmation: Berrespena - field_version: Bertsioa - field_type: Mota - field_host: Ostalaria - field_port: Portua - field_account: Kontua - field_base_dn: Base DN - field_attr_login: Erabiltzaile atributua - field_attr_firstname: Izena atributua - field_attr_lastname: Abizenak atributua - field_attr_mail: Eposta atributua - field_onthefly: Zuzeneko erabiltzaile sorrera - field_start_date: Hasiera - field_done_ratio: Egindako % - field_auth_source: Autentikazio modua - field_hide_mail: Nire eposta helbidea ezkutatu - field_comments: Iruzkina - field_url: URL - field_start_page: Hasierako orria - field_subproject: Azpiproiektua - field_hours: Ordu - field_activity: Jarduera - field_spent_on: Data - field_identifier: Identifikatzailea - field_is_filter: Iragazki moduan erabilita - field_issue_to: Erlazionatutako zereginak - field_delay: Atzerapena - field_assignable: Arazoak rol honetara esleitu daitezke - field_redirect_existing_links: Existitzen diren estekak berbideratu - field_estimated_hours: Estimatutako denbora - field_column_names: Zutabeak - field_time_zone: Ordu zonaldea - field_searchable: Bilagarria - field_default_value: Lehenetsitako balioa - field_comments_sorting: Iruzkinak erakutsi - field_parent_title: Orri gurasoa - field_editable: Editagarria - field_watcher: Behatzailea - field_identity_url: OpenID URLa - field_content: Edukia - field_group_by: Emaitzak honegatik taldekatu - field_sharing: Partekatzea - - setting_app_title: Aplikazioaren izenburua - setting_app_subtitle: Aplikazioaren azpizenburua - setting_welcome_text: Ongietorriko testua - setting_default_language: Lehenetsitako hizkuntza - setting_login_required: Autentikazioa derrigorrezkoa - setting_self_registration: Norberak erregistratu - setting_attachment_max_size: Eranskinen tamaina max. - setting_issues_export_limit: Zereginen esportatze limitea - setting_mail_from: Igorlearen eposta helbidea - setting_bcc_recipients: Hartzaileak ezkutuko kopian (bcc) - setting_plain_text_mail: Testu soileko epostak (HTML-rik ez) - setting_host_name: Ostalari izena eta bidea - setting_text_formatting: Testu formatua - setting_wiki_compression: Wikiaren historia konprimitu - setting_feeds_limit: Jarioaren edukiera limitea - setting_default_projects_public: Proiektu berriak defektuz publikoak dira - setting_autofetch_changesets: Commit-ak automatikoki hartu - setting_sys_api_enabled: Biltegien kudeaketarako WS gaitu - setting_commit_ref_keywords: Erreferentzien gako-hitzak - setting_commit_fix_keywords: Konpontze gako-hitzak - setting_autologin: Saioa automatikoki hasi - setting_date_format: Data formatua - setting_time_format: Ordu formatua - setting_cross_project_issue_relations: Zereginak proiektuen artean erlazionatzea baimendu - setting_issue_list_default_columns: Zereginen zerrendan defektuz ikusten diren zutabeak - setting_emails_footer: Eposten oina - setting_protocol: Protokoloa - setting_per_page_options: Orriko objektuen aukerak - setting_user_format: Erabiltzaileak erakusteko formatua - setting_activity_days_default: Proiektuen jardueran erakusteko egunak - setting_display_subprojects_issues: Azpiproiektuen zereginak proiektu nagusian erakutsi defektuz - setting_enabled_scm: Gaitutako IKKak - setting_mail_handler_body_delimiters: "Lerro hauteko baten ondoren epostak moztu" - setting_mail_handler_api_enabled: Sarrerako epostentzako WS gaitu - setting_mail_handler_api_key: API giltza - setting_sequential_project_identifiers: Proiektuen identifikadore sekuentzialak sortu - setting_gravatar_enabled: Erabili Gravatar erabiltzaile ikonoak - setting_gravatar_default: Lehenetsitako Gravatar irudia - setting_diff_max_lines_displayed: Erakutsiko diren diff lerro kopuru maximoa - setting_file_max_size_displayed: Barnean erakuzten diren testu fitxategien tamaina maximoa - setting_repository_log_display_limit: Egunkari fitxategian erakutsiko diren berrikuspen kopuru maximoa. - setting_openid: Baimendu OpenID saio hasiera eta erregistatzea - setting_password_min_length: Pasahitzen luzera minimoa - setting_new_project_user_role_id: Proiektu berriak sortzerakoan kudeatzaile ez diren erabiltzaileei esleitutako rola - setting_default_projects_modules: Proiektu berrientzako defektuz gaitutako moduluak - setting_issue_done_ratio: "Zereginen burututako tasa kalkulatzean erabili:" - setting_issue_done_ratio_issue_field: Zeregin eremua erabili - setting_issue_done_ratio_issue_status: Zeregin egoera erabili - setting_start_of_week: "Egutegiak noiz hasi:" - setting_rest_api_enabled: Gaitu REST web zerbitzua - - permission_add_project: Proiektua sortu - permission_add_subprojects: Azpiproiektuak sortu - permission_edit_project: Proiektua editatu - permission_select_project_modules: Proiektuaren moduluak hautatu - permission_manage_members: Kideak kudeatu - permission_manage_versions: Bertsioak kudeatu - permission_manage_categories: Arazoen kategoriak kudeatu - permission_view_issues: Zereginak ikusi - permission_add_issues: Zereginak gehitu - permission_edit_issues: Zereginak aldatu - permission_manage_issue_relations: Zereginen erlazioak kudeatu - permission_add_issue_notes: Oharrak gehitu - permission_edit_issue_notes: Oharrak aldatu - permission_edit_own_issue_notes: Nork bere oharrak aldatu - permission_move_issues: Zereginak mugitu - permission_delete_issues: Zereginak ezabatu - permission_manage_public_queries: Galdera publikoak kudeatu - permission_save_queries: Galderak gorde - permission_view_gantt: Gantt grafikoa ikusi - permission_view_calendar: Egutegia ikusi - permission_view_issue_watchers: Behatzaileen zerrenda ikusi - permission_add_issue_watchers: Behatzaileak gehitu - permission_delete_issue_watchers: Behatzaileak ezabatu - permission_log_time: Igarotako denbora erregistratu - permission_view_time_entries: Igarotako denbora ikusi - permission_edit_time_entries: Denbora egunkariak editatu - permission_edit_own_time_entries: Nork bere denbora egunkariak editatu - permission_manage_news: Berriak kudeatu - permission_comment_news: Berrien iruzkinak egin - permission_manage_documents: Dokumentuak kudeatu - permission_view_documents: Dokumentuak ikusi - permission_manage_files: Fitxategiak kudeatu - permission_view_files: Fitxategiak ikusi - permission_manage_wiki: Wikia kudeatu - permission_rename_wiki_pages: Wiki orriak berrizendatu - permission_delete_wiki_pages: Wiki orriak ezabatu - permission_view_wiki_pages: Wikia ikusi - permission_view_wiki_edits: Wikiaren historia ikusi - permission_edit_wiki_pages: Wiki orriak editatu - permission_delete_wiki_pages_attachments: Eranskinak ezabatu - permission_protect_wiki_pages: Wiki orriak babestu - permission_manage_repository: Biltegiak kudeatu - permission_browse_repository: Biltegia arakatu - permission_view_changesets: Aldaketak ikusi - permission_commit_access: Commit atzipena - permission_manage_boards: Foroak kudeatu - permission_view_messages: Mezuak ikusi - permission_add_messages: Mezuak bidali - permission_edit_messages: Mezuak aldatu - permission_edit_own_messages: Nork bere mezuak aldatu - permission_delete_messages: Mezuak ezabatu - permission_delete_own_messages: Nork bere mezuak ezabatu - - project_module_issue_tracking: Zereginen jarraipena - project_module_time_tracking: Denbora jarraipena - project_module_news: Berriak - project_module_documents: Dokumentuak - project_module_files: Fitxategiak - project_module_wiki: Wiki - project_module_repository: Biltegia - project_module_boards: Foroak - - label_user: Erabiltzailea - label_user_plural: Erabiltzaileak - label_user_new: Erabiltzaile berria - label_user_anonymous: Ezezaguna - label_project: Proiektua - label_project_new: Proiektu berria - label_project_plural: Proiektuak - label_x_projects: - zero: proiekturik ez - one: proiektu bat - other: "%{count} proiektu" - label_project_all: Proiektu guztiak - label_project_latest: Azken proiektuak - label_issue: Zeregina - label_issue_new: Zeregin berria - label_issue_plural: Zereginak - label_issue_view_all: Zeregin guztiak ikusi - label_issues_by: "Zereginak honengatik: %{value}" - label_issue_added: Zeregina gehituta - label_issue_updated: Zeregina eguneratuta - label_document: Dokumentua - label_document_new: Dokumentu berria - label_document_plural: Dokumentuak - label_document_added: Dokumentua gehituta - label_role: Rola - label_role_plural: Rolak - label_role_new: Rol berria - label_role_and_permissions: Rolak eta baimenak - label_member: Kidea - label_member_new: Kide berria - label_member_plural: Kideak - label_tracker: Aztarnaria - label_tracker_plural: Aztarnariak - label_tracker_new: Aztarnari berria - label_workflow: Lan-fluxua - label_issue_status: Zeregin egoera - label_issue_status_plural: Zeregin egoerak - label_issue_status_new: Egoera berria - label_issue_category: Zeregin kategoria - label_issue_category_plural: Zeregin kategoriak - label_issue_category_new: Kategoria berria - label_custom_field: Eremu pertsonalizatua - label_custom_field_plural: Eremu pertsonalizatuak - label_custom_field_new: Eremu pertsonalizatu berria - label_enumerations: Enumerazioak - label_enumeration_new: Balio berria - label_information: Informazioa - label_information_plural: Informazioa - label_please_login: Saioa hasi mesedez - label_register: Erregistratu - label_login_with_open_id_option: edo OpenID-rekin saioa hasi - label_password_lost: Pasahitza galduta - label_home: Hasiera - label_my_page: Nire orria - label_my_account: Nire kontua - label_my_projects: Nire proiektuak - label_administration: Kudeaketa - label_login: Saioa hasi - label_logout: Saioa bukatu - label_help: Laguntza - label_reported_issues: Berri emandako zereginak - label_assigned_to_me_issues: Niri esleitutako arazoak - label_last_login: Azken konexioa - label_registered_on: Noiz erregistratuta - label_activity: Jarduerak - label_overall_activity: Jarduera guztiak - label_user_activity: "%{value}-(r)en jarduerak" - label_new: Berria - label_logged_as: "Sartutako erabiltzailea:" - label_environment: Ingurune - label_authentication: Autentikazioa - label_auth_source: Autentikazio modua - label_auth_source_new: Autentikazio modu berria - label_auth_source_plural: Autentikazio moduak - label_subproject_plural: Azpiproiektuak - label_subproject_new: Azpiproiektu berria - label_and_its_subprojects: "%{value} eta bere azpiproiektuak" - label_min_max_length: Luzera min - max - label_list: Zerrenda - label_date: Data - label_integer: Osokoa - label_float: Koma higikorrekoa - label_boolean: Boolearra - label_string: Testua - label_text: Testu luzea - label_attribute: Atributua - label_attribute_plural: Atributuak - label_download: "Deskarga %{count}" - label_download_plural: "%{count} Deskarga" - label_no_data: Ez dago erakusteko daturik - label_change_status: Egoera aldatu - label_history: Historikoa - label_attachment: Fitxategia - label_attachment_new: Fitxategi berria - label_attachment_delete: Fitxategia ezabatu - label_attachment_plural: Fitxategiak - label_file_added: Fitxategia gehituta - label_report: Berri ematea - label_report_plural: Berri emateak - label_news: Berria - label_news_new: Berria gehitu - label_news_plural: Berriak - label_news_latest: Azken berriak - label_news_view_all: Berri guztiak ikusi - label_news_added: Berria gehituta - label_settings: Ezarpenak - label_overview: Gainbegirada - label_version: Bertsioa - label_version_new: Bertsio berria - label_version_plural: Bertsioak - label_close_versions: Burututako bertsioak itxi - label_confirmation: Baieztapena - label_export_to: 'Eskuragarri baita:' - label_read: Irakurri... - label_public_projects: Proiektu publikoak - label_open_issues: irekita - label_open_issues_plural: irekiak - label_closed_issues: itxita - label_closed_issues_plural: itxiak - label_x_open_issues_abbr_on_total: - zero: 0 irekita / %{total} - one: 1 irekita / %{total} - other: "%{count} irekiak / %{total}" - label_x_open_issues_abbr: - zero: 0 irekita - one: 1 irekita - other: "%{count} irekiak" - label_x_closed_issues_abbr: - zero: 0 itxita - one: 1 itxita - other: "%{count} itxiak" - label_total: Guztira - label_permissions: Baimenak - label_current_status: Uneko egoera - label_new_statuses_allowed: Baimendutako egoera berriak - label_all: guztiak - label_none: ezer - label_nobody: inor - label_next: Hurrengoa - label_previous: Aurrekoak - label_used_by: Erabilita - label_details: Xehetasunak - label_add_note: Oharra gehitu - label_per_page: Orriko - label_calendar: Egutegia - label_months_from: hilabete noiztik - label_gantt: Gantt - label_internal: Barnekoa - label_last_changes: "azken %{count} aldaketak" - label_change_view_all: Aldaketa guztiak ikusi - label_personalize_page: Orri hau pertsonalizatu - label_comment: Iruzkin - label_comment_plural: Iruzkinak - label_x_comments: - zero: iruzkinik ez - one: iruzkin 1 - other: "%{count} iruzkin" - label_comment_add: Iruzkina gehitu - label_comment_added: Iruzkina gehituta - label_comment_delete: Iruzkinak ezabatu - label_query: Galdera pertsonalizatua - label_query_plural: Pertsonalizatutako galderak - label_query_new: Galdera berria - label_filter_add: Iragazkia gehitu - label_filter_plural: Iragazkiak - label_equals: da - label_not_equals: ez da - label_in_less_than: baino gutxiagotan - label_in_more_than: baino gehiagotan - label_greater_or_equal: '>=' - label_less_or_equal: '<=' - label_in: hauetan - label_today: gaur - label_all_time: denbora guztia - label_yesterday: atzo - label_this_week: aste honetan - label_last_week: pasadan astean - label_last_n_days: "azken %{count} egunetan" - label_this_month: hilabete hau - label_last_month: pasadan hilabetea - label_this_year: urte hau - label_date_range: Data tartea - label_less_than_ago: egun hauek baino gutxiago - label_more_than_ago: egun hauek baino gehiago - label_ago: orain dela - label_contains: dauka - label_not_contains: ez dauka - label_day_plural: egun - label_repository: Biltegia - label_repository_plural: Biltegiak - label_browse: Arakatu - label_modification: "aldaketa %{count}" - label_modification_plural: "%{count} aldaketa" - label_branch: Adarra - label_tag: Etiketa - label_revision: Berrikuspena - label_revision_plural: Berrikuspenak - label_revision_id: "%{value} berrikuspen" - label_associated_revisions: Elkartutako berrikuspenak - label_added: gehituta - label_modified: aldatuta - label_copied: kopiatuta - label_renamed: berrizendatuta - label_deleted: ezabatuta - label_latest_revision: Azken berrikuspena - label_latest_revision_plural: Azken berrikuspenak - label_view_revisions: Berrikuspenak ikusi - label_view_all_revisions: Berrikuspen guztiak ikusi - label_max_size: Tamaina maximoa - label_sort_highest: Goraino mugitu - label_sort_higher: Gora mugitu - label_sort_lower: Behera mugitu - label_sort_lowest: Beheraino mugitu - label_roadmap: Ibilbide-mapa - label_roadmap_due_in: "Epea: %{value}" - label_roadmap_overdue: "%{value} berandu" - label_roadmap_no_issues: Ez dago zereginik bertsio honetan - label_search: Bilatu - label_result_plural: Emaitzak - label_all_words: hitz guztiak - label_wiki: Wikia - label_wiki_edit: Wiki edizioa - label_wiki_edit_plural: Wiki edizioak - label_wiki_page: Wiki orria - label_wiki_page_plural: Wiki orriak - label_index_by_title: Izenburuaren araberako indizea - label_index_by_date: Dataren araberako indizea - label_current_version: Uneko bertsioa - label_preview: Aurreikusi - label_feed_plural: Jarioak - label_changes_details: Aldaketa guztien xehetasunak - label_issue_tracking: Zeregin jarraipena - label_spent_time: Igarotako denbora - label_f_hour: "ordu %{value}" - label_f_hour_plural: "%{value} ordu" - label_time_tracking: Denbora jarraipena - label_change_plural: Aldaketak - label_statistics: Estatistikak - label_commits_per_month: Commit-ak hilabeteka - label_commits_per_author: Commit-ak egileka - label_view_diff: Ezberdintasunak ikusi - label_diff_inline: barnean - label_diff_side_by_side: aldez alde - label_options: Aukerak - label_copy_workflow_from: Kopiatu workflow-a hemendik - label_permissions_report: Baimenen txostena - label_watched_issues: Behatutako zereginak - label_related_issues: Erlazionatutako zereginak - label_applied_status: Aplikatutako egoera - label_loading: Kargatzen... - label_relation_new: Erlazio berria - label_relation_delete: Erlazioa ezabatu - label_relates_to: erlazionatuta dago - label_duplicates: bikoizten du - label_duplicated_by: honek bikoiztuta - label_blocks: blokeatzen du - label_blocked_by: honek blokeatuta - label_precedes: aurretik doa - label_follows: jarraitzen du - label_end_to_start: bukaeratik hasierara - label_end_to_end: bukaeratik bukaerara - label_start_to_start: hasieratik hasierhasieratik bukaerara - label_start_to_end: hasieratik bukaerara - label_stay_logged_in: Saioa mantendu - label_disabled: ezgaituta - label_show_completed_versions: Bukatutako bertsioak ikusi - label_me: ni - label_board: Foroa - label_board_new: Foro berria - label_board_plural: Foroak - label_topic_plural: Gaiak - label_message_plural: Mezuak - label_message_last: Azken mezua - label_message_new: Mezu berria - label_message_posted: Mezua gehituta - label_reply_plural: Erantzunak - label_send_information: Erabiltzaileai kontuaren informazioa bidali - label_year: Urtea - label_month: Hilabetea - label_week: Astea - label_date_from: Nork - label_date_to: Nori - label_language_based: Erabiltzailearen hizkuntzaren arabera - label_sort_by: "Ordenazioa: %{value}" - label_send_test_email: Frogako mezua bidali - label_feeds_access_key: RSS atzipen giltza - label_missing_feeds_access_key: RSS atzipen giltza falta da - label_feeds_access_key_created_on: "RSS atzipen giltza orain dela %{value} sortuta" - label_module_plural: Moduluak - label_added_time_by: "%{author}, orain dela %{age} gehituta" - label_updated_time_by: "%{author}, orain dela %{age} eguneratuta" - label_updated_time: "Orain dela %{value} eguneratuta" - label_jump_to_a_project: Joan proiektura... - label_file_plural: Fitxategiak - label_changeset_plural: Aldaketak - label_default_columns: Lehenetsitako zutabeak - label_no_change_option: (Aldaketarik ez) - label_bulk_edit_selected_issues: Hautatutako zereginak batera editatu - label_theme: Itxura - label_default: Lehenetsia - label_search_titles_only: Izenburuetan bakarrik bilatu - label_user_mail_option_all: "Nire proiektu guztietako gertakari guztientzat" - label_user_mail_option_selected: "Hautatutako proiektuetako edozein gertakarientzat..." - label_user_mail_no_self_notified: "Ez dut nik egiten ditudan aldeketen jakinarazpenik jaso nahi" - label_registration_activation_by_email: kontuak epostaz gaitu - label_registration_manual_activation: kontuak eskuz gaitu - label_registration_automatic_activation: kontuak automatikoki gaitu - label_display_per_page: "Orriko: %{value}" - label_age: Adina - label_change_properties: Propietateak aldatu - label_general: Orokorra - label_more: Gehiago - label_scm: IKK - label_plugins: Pluginak - label_ldap_authentication: LDAP autentikazioa - label_downloads_abbr: Desk. - label_optional_description: Aukerako deskribapena - label_add_another_file: Beste fitxategia gehitu - label_preferences: Hobespenak - label_chronological_order: Orden kronologikoan - label_reverse_chronological_order: Alderantzizko orden kronologikoan - label_planning: Planifikazioa - label_incoming_emails: Sarrerako epostak - label_generate_key: Giltza sortu - label_issue_watchers: Behatzaileak - label_example: Adibidea - label_display: Bistaratzea - label_sort: Ordenatu - label_ascending: Gorantz - label_descending: Beherantz - label_date_from_to: "%{start}-tik %{end}-ra" - label_wiki_content_added: Wiki orria gehituta - label_wiki_content_updated: Wiki orria eguneratuta - label_group: Taldea - label_group_plural: Taldeak - label_group_new: Talde berria - label_time_entry_plural: Igarotako denbora - label_version_sharing_none: Ez partekatuta - label_version_sharing_descendants: Azpiproiektuekin - label_version_sharing_hierarchy: Proiektu Hierarkiarekin - label_version_sharing_tree: Proiektu zuhaitzarekin - label_version_sharing_system: Proiektu guztiekin - label_update_issue_done_ratios: Zereginen burututako erlazioa eguneratu - label_copy_source: Iturburua - label_copy_target: Helburua - label_copy_same_as_target: Helburuaren berdina - label_display_used_statuses_only: Aztarnari honetan erabiltzen diren egoerak bakarrik erakutsi - label_api_access_key: API atzipen giltza - label_missing_api_access_key: API atzipen giltza falta da - label_api_access_key_created_on: "API atzipen giltza sortuta orain dela %{value}" - - button_login: Saioa hasi - button_submit: Bidali - button_save: Gorde - button_check_all: Guztiak markatu - button_uncheck_all: Guztiak desmarkatu - button_delete: Ezabatu - button_create: Sortu - button_create_and_continue: Sortu eta jarraitu - button_test: Frogatu - button_edit: Editatu - button_add: Gehitu - button_change: Aldatu - button_apply: Aplikatu - button_clear: Garbitu - button_lock: Blokeatu - button_unlock: Desblokeatu - button_download: Deskargatu - button_list: Zerrenda - button_view: Ikusi - button_move: Mugitu - button_move_and_follow: Mugitu eta jarraitu - button_back: Atzera - button_cancel: Ezeztatu - button_activate: Gahitu - button_sort: Ordenatu - button_log_time: Denbora erregistratu - button_rollback: Itzuli bertsio honetara - button_watch: Behatu - button_unwatch: Behatzen utzi - button_reply: Erantzun - button_archive: Artxibatu - button_unarchive: Desartxibatu - button_reset: Berrezarri - button_rename: Berrizendatu - button_change_password: Pasahitza aldatu - button_copy: Kopiatu - button_copy_and_follow: Kopiatu eta jarraitu - button_annotate: Anotatu - button_update: Eguneratu - button_configure: Konfiguratu - button_quote: Aipatu - button_duplicate: Bikoiztu - button_show: Ikusi - - status_active: gaituta - status_registered: izena emanda - status_locked: blokeatuta - - version_status_open: irekita - version_status_locked: blokeatuta - version_status_closed: itxita - - field_active: Gaituta - - text_select_mail_notifications: Jakinarazpenak zein ekintzetarako bidaliko diren hautatu. - text_regexp_info: adib. ^[A-Z0-9]+$ - text_min_max_length_info: 0k mugarik gabe esan nahi du - text_project_destroy_confirmation: Ziur zaude proiektu hau eta erlazionatutako datu guztiak ezabatu nahi dituzula? - text_subprojects_destroy_warning: "%{value} azpiproiektuak ere ezabatuko dira." - text_workflow_edit: Hautatu rola eta aztarnaria workflow-a editatzeko - text_are_you_sure: Ziur zaude? - text_journal_changed: "%{label} %{old}-(e)tik %{new}-(e)ra aldatuta" - text_journal_set_to: "%{label}-k %{value} balioa hartu du" - text_journal_deleted: "%{label} ezabatuta (%{old})" - text_journal_added: "%{label} %{value} gehituta" - text_tip_issue_begin_day: gaur hasten diren zereginak - text_tip_issue_end_day: gaur bukatzen diren zereginak - text_tip_issue_begin_end_day: gaur hasi eta bukatzen diren zereginak - text_caracters_maximum: "%{count} karaktere gehienez." - text_caracters_minimum: "Gutxienez %{count} karaktereetako luzerakoa izan behar du." - text_length_between: "Luzera %{min} eta %{max} karaktereen artekoa." - text_tracker_no_workflow: Ez da workflow-rik definitu aztarnari honentzako - text_unallowed_characters: Debekatutako karaktereak - text_comma_separated: Balio anitz izan daitezke (komaz banatuta). - text_line_separated: Balio anitz izan daitezke (balio bakoitza lerro batean). - text_issues_ref_in_commit_messages: Commit-en mezuetan zereginak erlazionatu eta konpontzen - text_issue_added: "%{id} zeregina %{author}-(e)k jakinarazi du." - text_issue_updated: "%{id} zeregina %{author}-(e)k eguneratu du." - text_wiki_destroy_confirmation: Ziur zaude wiki hau eta bere eduki guztiak ezabatu nahi dituzula? - text_issue_category_destroy_question: "Zeregin batzuk (%{count}) kategoria honetara esleituta daude. Zer egin nahi duzu?" - text_issue_category_destroy_assignments: Kategoria esleipenak kendu - text_issue_category_reassign_to: Zereginak kategoria honetara esleitu - text_user_mail_option: "Hautatu gabeko proiektuetan, behatzen edo parte hartzen duzun gauzei buruzko jakinarazpenak jasoko dituzu (adib. zu egile zaren edo esleituta dituzun zereginak)." - text_no_configuration_data: "Rolak, aztarnariak, zeregin egoerak eta workflow-ak ez dira oraindik konfiguratu.\nOso gomendagarria de lehenetsitako kkonfigurazioa kargatzea. Kargatu eta gero aldatu ahalko duzu." - text_load_default_configuration: Lehenetsitako konfigurazioa kargatu - text_status_changed_by_changeset: "%{value} aldaketan aplikatuta." - text_issues_destroy_confirmation: 'Ziur zaude hautatutako zeregina(k) ezabatu nahi dituzula?' - text_select_project_modules: 'Hautatu proiektu honetan gaitu behar diren moduluak:' - text_default_administrator_account_changed: Lehenetsitako kudeatzaile kontua aldatuta - text_file_repository_writable: Eranskinen direktorioan idatz daiteke - text_plugin_assets_writable: Pluginen baliabideen direktorioan idatz daiteke - text_rmagick_available: RMagick eskuragarri (aukerazkoa) - text_destroy_time_entries_question: "%{hours} orduei buruz berri eman zen zuk ezabatzera zoazen zereginean. Zer egin nahi duzu?" - text_destroy_time_entries: Ezabatu berri emandako orduak - text_assign_time_entries_to_project: Berri emandako orduak proiektura esleitu - text_reassign_time_entries: 'Berri emandako orduak zeregin honetara esleitu:' - text_user_wrote: "%{value}-(e)k idatzi zuen:" - text_enumeration_destroy_question: "%{count} objetu balio honetara esleituta daude." - text_enumeration_category_reassign_to: 'Beste balio honetara esleitu:' - text_email_delivery_not_configured: "Eposta bidalketa ez dago konfiguratuta eta jakinarazpenak ezgaituta daude.\nKonfiguratu zure SMTP zerbitzaria config/configuration.yml-n eta aplikazioa berrabiarazi hauek gaitzeko." - text_repository_usernames_mapping: "Hautatu edo eguneratu Redmineko erabiltzailea biltegiko egunkarietan topatzen diren erabiltzaile izenekin erlazionatzeko.\nRedmine-n eta biltegian erabiltzaile izen edo eposta berdina duten erabiltzaileak automatikoki erlazionatzen dira." - text_diff_truncated: '... Diff hau moztua izan da erakus daitekeen tamaina maximoa gainditu duelako.' - text_custom_field_possible_values_info: 'Lerro bat balio bakoitzeko' - text_wiki_page_destroy_question: "Orri honek %{descendants} orri seme eta ondorengo ditu. Zer egin nahi duzu?" - text_wiki_page_nullify_children: "Orri semeak erro orri moduan mantendu" - text_wiki_page_destroy_children: "Orri semeak eta beraien ondorengo guztiak ezabatu" - text_wiki_page_reassign_children: "Orri semeak orri guraso honetara esleitu" - text_own_membership_delete_confirmation: "Zure baimen batzuk (edo guztiak) kentzera zoaz eta baliteke horren ondoren proiektu hau ezin editatzea.\n Ziur zaude jarraitu nahi duzula?" - - default_role_manager: Kudeatzailea - default_role_developer: Garatzailea - default_role_reporter: Berriemailea - default_tracker_bug: Errorea - default_tracker_feature: Eginbidea - default_tracker_support: Laguntza - default_issue_status_new: Berria - default_issue_status_in_progress: Lanean - default_issue_status_resolved: Ebatzita - default_issue_status_feedback: Berrelikadura - default_issue_status_closed: Itxita - default_issue_status_rejected: Baztertua - default_doc_category_user: Erabiltzaile dokumentazioa - default_doc_category_tech: Dokumentazio teknikoa - default_priority_low: Baxua - default_priority_normal: Normala - default_priority_high: Altua - default_priority_urgent: Larria - default_priority_immediate: Berehalakoa - default_activity_design: Diseinua - default_activity_development: Garapena - - enumeration_issue_priorities: Zeregin lehentasunak - enumeration_doc_categories: Dokumentu kategoriak - enumeration_activities: Jarduerak (denbora kontrola)) - enumeration_system_activity: Sistemako Jarduera - label_board_sticky: Itsaskorra - label_board_locked: Blokeatuta - permission_export_wiki_pages: Wiki orriak esportatu - setting_cache_formatted_text: Formatudun testua katxeatu - permission_manage_project_activities: Proiektuaren jarduerak kudeatu - error_unable_delete_issue_status: Ezine da zereginaren egoera ezabatu - label_profile: Profila - permission_manage_subtasks: Azpiatazak kudeatu - field_parent_issue: Zeregin gurasoa - label_subtask_plural: Azpiatazak - label_project_copy_notifications: Proiektua kopiatzen den bitartean eposta jakinarazpenak bidali - error_can_not_delete_custom_field: Ezin da eremu pertsonalizatua ezabatu - error_unable_to_connect: Ezin da konektatu (%{value}) - error_can_not_remove_role: Rol hau erabiltzen hari da eta ezin da ezabatu. - error_can_not_delete_tracker: Aztarnari honek zereginak ditu eta ezin da ezabatu. - field_principal: Ekintzaile - label_my_page_block: "Nire orriko blokea" - notice_failed_to_save_members: "Kidea(k) gordetzean errorea: %{errors}." - text_zoom_out: Zooma txikiagotu - text_zoom_in: Zooma handiagotu - notice_unable_delete_time_entry: "Ezin da hautatutako denbora erregistroa ezabatu." - label_overall_spent_time: Igarotako denbora guztira - field_time_entries: "Denbora erregistratu" - project_module_gantt: Gantt - project_module_calendar: Egutegia - button_edit_associated_wikipage: "Esleitutako wiki orria editatu: %{page_title}" - field_text: Testu eremua - label_user_mail_option_only_owner: "Jabea naizen gauzetarako barrarik" - setting_default_notification_option: "Lehenetsitako ohartarazpen aukera" - label_user_mail_option_only_my_events: "Behatzen ditudan edo partaide naizen gauzetarako bakarrik" - label_user_mail_option_only_assigned: "Niri esleitutako gauzentzat bakarrik" - label_user_mail_option_none: "Gertakaririk ez" - field_member_of_group: "Esleituta duenaren taldea" - field_assigned_to_role: "Esleituta duenaren rola" - notice_not_authorized_archived_project: "Atzitu nahi duzun proiektua artxibatua izan da." - label_principal_search: "Bilatu erabiltzaile edo taldea:" - label_user_search: "Erabiltzailea bilatu:" - field_visible: Ikusgai - setting_emails_header: "Eposten goiburua" - setting_commit_logtime_activity_id: "Erregistratutako denboraren jarduera" - text_time_logged_by_changeset: "%{value} aldaketan egindakoa." - setting_commit_logtime_enabled: "Erregistrutako denbora gaitu" - notice_gantt_chart_truncated: Grafikoa moztu da bistara daitekeen elementuen kopuru maximoa gainditu delako (%{max}) - setting_gantt_items_limit: "Gantt grafikoan bistara daitekeen elementu kopuru maximoa" - field_warn_on_leaving_unsaved: Gorde gabeko testua duen orri batetik ateratzen naizenean ohartarazi - text_warn_on_leaving_unsaved: Uneko orritik joaten bazara gorde gabeko testua galduko da. - label_my_queries: Nire galdera pertsonalizatuak - text_journal_changed_no_detail: "%{label} eguneratuta" - label_news_comment_added: Berri batera iruzkina gehituta - button_expand_all: Guztia zabaldu - button_collapse_all: Guztia tolestu - label_additional_workflow_transitions_for_assignee: Erabiltzaileak esleitua duenean baimendutako transtsizio gehigarriak - label_additional_workflow_transitions_for_author: Erabiltzailea egilea denean baimendutako transtsizio gehigarriak - label_bulk_edit_selected_time_entries: Hautatutako denbora egunkariak batera editatu - text_time_entries_destroy_confirmation: Ziur zaude hautatutako denbora egunkariak ezabatu nahi dituzula? - label_role_anonymous: Ezezaguna - label_role_non_member: Ez kidea - label_issue_note_added: Oharra gehituta - label_issue_status_updated: Egoera eguneratuta - label_issue_priority_updated: Lehentasuna eguneratuta - label_issues_visibility_own: Erabiltzaileak sortu edo esleituta dituen zereginak - field_issues_visibility: Zeregin ikusgarritasuna - label_issues_visibility_all: Zeregin guztiak - permission_set_own_issues_private: Nork bere zereginak publiko edo pribatu jarri - field_is_private: Pribatu - permission_set_issues_private: Zereginak publiko edo pribatu jarri - label_issues_visibility_public: Pribatu ez diren zeregin guztiak - text_issues_destroy_descendants_confirmation: Honek %{count} azpiataza ezabatuko ditu baita ere. - field_commit_logs_encoding: Commit-en egunkarien kodetzea - field_scm_path_encoding: Bidearen kodeketa - text_scm_path_encoding_note: "Lehentsita: UTF-8" - field_path_to_repository: Biltegirako bidea - field_root_directory: Erro direktorioa - field_cvs_module: Modulua - field_cvsroot: CVSROOT - text_mercurial_repository_note: Biltegi locala (adib. /hgrepo, c:\hgrepo) - text_scm_command: Komandoa - text_scm_command_version: Bertsioa - label_git_report_last_commit: Report last commit for files and directories - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - 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 zeregina - one: 1 zeregina - other: "%{count} zereginak" - 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.
    Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account - text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: guztiak - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: Azpiproiektuekin - label_cross_project_tree: Proiektu zuhaitzarekin - label_cross_project_hierarchy: Proiektu Hierarkiarekin - label_cross_project_system: Proiektu guztiekin - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1b/1b2b1493975060c684a85d5ff8b6ad5b3c0ddaf7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b2b1493975060c684a85d5ff8b6ad5b3c0ddaf7.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1516 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 ApplicationHelperTest < ActionView::TestCase + include Redmine::I18n + include ERB::Util + include Rails.application.routes.url_helpers + + fixtures :projects, :roles, :enabled_modules, :users, + :repositories, :changesets, + :trackers, :issue_statuses, :issues, :versions, :documents, + :wikis, :wiki_pages, :wiki_contents, + :boards, :messages, :news, + :attachments, :enumerations + + def setup + super + set_tmp_attachments_directory + @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82" + if @russian_test.respond_to?(:force_encoding) + @russian_test.force_encoding('UTF-8') + end + end + + test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do + User.current = User.find_by_login('admin') + + @project = Issue.first.project # Used by helper + response = link_to_if_authorized('By controller/actionr', + {:controller => 'issues', :action => 'edit', :id => Issue.first.id}) + assert_match /href/, response + end + + test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do + User.current = User.find_by_login('dlopper') + @project = Project.find('private-child') + issue = @project.issues.first + assert !issue.visible? + + response = link_to_if_authorized('Never displayed', + {:controller => 'issues', :action => 'show', :id => issue}) + assert_nil response + end + + def test_auto_links + to_test = { + 'http://foo.bar' => 'http://foo.bar', + 'http://foo.bar/~user' => 'http://foo.bar/~user', + 'http://foo.bar.' => 'http://foo.bar.', + 'https://foo.bar.' => 'https://foo.bar.', + 'This is a link: http://foo.bar.' => 'This is a link: http://foo.bar.', + 'A link (eg. http://foo.bar).' => 'A link (eg. http://foo.bar).', + 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.', + 'http://www.foo.bar/Test_(foobar)' => 'http://www.foo.bar/Test_(foobar)', + '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : http://www.foo.bar/Test_(foobar))', + '(see inline link : http://www.foo.bar/Test)' => '(see inline link : http://www.foo.bar/Test)', + '(see inline link : http://www.foo.bar/Test).' => '(see inline link : http://www.foo.bar/Test).', + '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test)' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test).' => '(see inline link).', + 'www.foo.bar' => 'www.foo.bar', + 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', + 'http://foo.bar/page#125' => 'http://foo.bar/page#125', + 'http://foo@www.bar.com' => 'http://foo@www.bar.com', + 'http://foo:bar@www.bar.com' => 'http://foo:bar@www.bar.com', + 'ftp://foo.bar' => 'ftp://foo.bar', + 'ftps://foo.bar' => 'ftps://foo.bar', + 'sftp://foo.bar' => 'sftp://foo.bar', + # two exclamation marks + 'http://example.net/path!602815048C7B5C20!302.html' => 'http://example.net/path!602815048C7B5C20!302.html', + # escaping + 'http://foo"bar' => 'http://foo"bar', + # wrap in angle brackets + '' => '<http://foo.bar>', + # invalid urls + 'http://' => 'http://', + 'www.' => 'www.', + 'test-www.bar.com' => 'test-www.bar.com', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_auto_links_with_non_ascii_characters + to_test = { + "http://foo.bar/#{@russian_test}" => + %|http://foo.bar/#{@russian_test}| + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_auto_mailto + to_test = { + 'test@foo.bar' => '', + 'test@www.foo.bar' => '', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_inline_images + to_test = { + '!http://foo.bar/image.jpg!' => '', + 'floating !>http://foo.bar/image.jpg!' => 'floating
    ', + 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class ', + 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style ', + 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title This is a title', + 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title This is a double-quoted "title"', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_inline_images_inside_tags + raw = <<-RAW +h1. !foo.png! Heading + +Centered image: + +p=. !bar.gif! +RAW + + assert textilizable(raw).include?('') + assert textilizable(raw).include?('') + end + + def test_attached_images + to_test = { + 'Inline image: !logo.gif!' => 'Inline image: This is a logo', + 'Inline image: !logo.GIF!' => 'Inline image: This is a logo', + 'No match: !ogo.gif!' => 'No match: ', + 'No match: !ogo.GIF!' => 'No match: ', + # link image + '!logo.gif!:http://foo.bar/' => 'This is a logo', + } + attachments = Attachment.all + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_filename_extension + set_tmp_attachments_directory + a1 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPG"}), + :author => User.find(1)) + assert a1.save + assert_equal "testtest.JPG", a1.filename + assert_equal "image/jpeg", a1.content_type + assert a1.image? + + a2 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.jpeg"}), + :author => User.find(1)) + assert a2.save + assert_equal "testtest.jpeg", a2.filename + assert_equal "image/jpeg", a2.content_type + assert a2.image? + + a3 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPE"}), + :author => User.find(1)) + assert a3.save + assert_equal "testtest.JPE", a3.filename + assert_equal "image/jpeg", a3.content_type + assert a3.image? + + a4 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "Testtest.BMP"}), + :author => User.find(1)) + assert a4.save + assert_equal "Testtest.BMP", a4.filename + assert_equal "image/x-ms-bmp", a4.content_type + assert a4.image? + + to_test = { + 'Inline image: !testtest.jpg!' => + 'Inline image: ', + 'Inline image: !testtest.jpeg!' => + 'Inline image: ', + 'Inline image: !testtest.jpe!' => + 'Inline image: ', + 'Inline image: !testtest.bmp!' => + 'Inline image: ', + } + + attachments = [a1, a2, a3, a4] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_should_read_later + set_fixtures_attachments_directory + a1 = Attachment.find(16) + assert_equal "testfile.png", a1.filename + assert a1.readable? + assert (! a1.visible?(User.anonymous)) + assert a1.visible?(User.find(2)) + a2 = Attachment.find(17) + assert_equal "testfile.PNG", a2.filename + assert a2.readable? + assert (! a2.visible?(User.anonymous)) + assert a2.visible?(User.find(2)) + assert a1.created_on < a2.created_on + + to_test = { + 'Inline image: !testfile.png!' => + 'Inline image: ', + 'Inline image: !Testfile.PNG!' => + 'Inline image: ', + } + attachments = [a1, a2] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + set_tmp_attachments_directory + end + + def test_textile_external_links + to_test = { + 'This is a "link":http://foo.bar' => 'This is a link', + 'This is an intern "link":/foo/bar' => 'This is an intern link', + '"link (Link title)":http://foo.bar' => 'link', + '"link (Link title with "double-quotes")":http://foo.bar' => 'link', + "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":

    \n\n\n\t

    Another paragraph", + # no multiline link text + "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line
    and another on a second line\":test", + # mailto link + "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "system administrator", + # two exclamation marks + '"a link":http://example.net/path!602815048C7B5C20!302.html' => 'a link', + # escaping + '"test":http://foo"bar' => 'test', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_textile_external_links_with_non_ascii_characters + to_test = { + %|This is a "link":http://foo.bar/#{@russian_test}| => + %|This is a link| + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_redmine_links + issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, + :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)') + note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'}, + :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)') + note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'}, + :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)') + + revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, + :class => 'changeset', :title => 'My very first commit do not escaping #<>&') + revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + changeset_link2 = link_to('691322a8eb01e11fd7', + {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, + :class => 'changeset', :title => 'My very first commit do not escaping #<>&') + + document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1}, + :class => 'document') + + version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2}, + :class => 'version') + + board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'} + + message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4} + + news_url = {:controller => 'news', :action => 'show', :id => 1} + + project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'} + + source_url = '/projects/ecookbook/repository/entry/some/file' + source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file' + source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext' + source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext' + source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file' + + export_url = '/projects/ecookbook/repository/raw/some/file' + export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file' + export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext' + export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext' + export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file' + + to_test = { + # tickets + '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.", + # ticket notes + '#3-14' => note_link, + '#3#note-14' => note_link2, + # should not ignore leading zero + '#03' => '#03', + # changesets + 'r1' => revision_link, + 'r1.' => "#{revision_link}.", + 'r1, r2' => "#{revision_link}, #{revision_link2}", + 'r1,r2' => "#{revision_link},#{revision_link2}", + 'commit:691322a8eb01e11fd7' => changeset_link2, + # documents + 'document#1' => document_link, + 'document:"Test document"' => document_link, + # versions + 'version#2' => version_link, + 'version:1.0' => version_link, + 'version:"1.0"' => version_link, + # source + 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'), + 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), + 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", + 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'), + 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'), + 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'), + 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'), + 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'), + 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'), + # export + 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'), + 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'), + 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'), + 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'), + 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'), + # forum + 'forum#2' => link_to('Discussion', board_url, :class => 'board'), + 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'), + # message + 'message#4' => link_to('Post 2', message_url, :class => 'message'), + 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'), + # news + 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'), + 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'), + # project + 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + # not found + '#0123456789' => '#0123456789', + # invalid expressions + 'source:' => 'source:', + # url hash + "http://foo.bar/FAQ#3" => 'http://foo.bar/FAQ#3', + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_with_a_different_project_before_current_project + vp1 = Version.generate!(:project_id => 1, :name => '1.4.4') + vp3 = Version.generate!(:project_id => 3, :name => '1.4.4') + @project = Project.find(3) + result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version") + result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version") + assert_equal "

    #{result1} #{result2}

    ", + textilizable("ecookbook:version:1.4.4 version:1.4.4") + end + + def test_escaped_redmine_links_should_not_be_parsed + to_test = [ + '#3.', + '#3-14.', + '#3#-note14.', + 'r1', + 'document#1', + 'document:"Test document"', + 'version#2', + 'version:1.0', + 'version:"1.0"', + 'source:/some/file' + ] + @project = Project.find(1) + to_test.each { |text| assert_equal "

    #{text}

    ", textilizable("!" + text), "#{text} failed" } + end + + def test_cross_project_redmine_links + source_link = link_to('ecookbook:source:/some/file', + {:controller => 'repositories', :action => 'entry', + :id => 'ecookbook', :path => ['some', 'file']}, + :class => 'source') + changeset_link = link_to('ecookbook:r2', + {:controller => 'repositories', :action => 'revision', + :id => 'ecookbook', :rev => 2}, + :class => 'changeset', + :title => 'This commit fixes #1, #2 and references #1 & #3') + to_test = { + # documents + 'document:"Test document"' => 'document:"Test document"', + 'ecookbook:document:"Test document"' => + link_to("Test document", "/documents/1", :class => "document"), + 'invalid:document:"Test document"' => 'invalid:document:"Test document"', + # versions + 'version:"1.0"' => 'version:"1.0"', + 'ecookbook:version:"1.0"' => + link_to("1.0", "/versions/2", :class => "version"), + 'invalid:version:"1.0"' => 'invalid:version:"1.0"', + # changeset + 'r2' => 'r2', + 'ecookbook:r2' => changeset_link, + 'invalid:r2' => 'invalid:r2', + # source + 'source:/some/file' => 'source:/some/file', + 'ecookbook:source:/some/file' => source_link, + 'invalid:source:/some/file' => 'invalid:source:/some/file', + } + @project = Project.find(3) + to_test.each do |text, result| + assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" + end + end + + def test_redmine_links_by_name_should_work_with_html_escaped_characters + v = Version.generate!(:name => "Test & Show.txt", :project_id => 1) + link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version") + + @project = v.project + assert_equal "

    #{link}

    ", textilizable('version:"Test & Show.txt"') + end + + def test_link_to_issue_subject + issue = Issue.generate!(:subject => "01234567890123456789") + str = link_to_issue(issue, :truncate => 10) + result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes) + assert_equal "#{result}: 0123456...", str + + issue = Issue.generate!(:subject => "<&>") + str = link_to_issue(issue) + result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes) + assert_equal "#{result}: <&>", str + + issue = Issue.generate!(:subject => "<&>0123456789012345") + str = link_to_issue(issue, :truncate => 10) + result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes) + assert_equal "#{result}: <&>0123...", str + end + + def test_link_to_issue_title + long_str = "0123456789" * 5 + + issue = Issue.generate!(:subject => "#{long_str}01234567890123456789") + str = link_to_issue(issue, :subject => false) + result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", + :class => issue.css_classes, + :title => "#{long_str}0123456...") + assert_equal result, str + + issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789") + str = link_to_issue(issue, :subject => false) + result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", + :class => issue.css_classes, + :title => "<&>#{long_str}0123...") + assert_equal result, str + end + + def test_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'r2' => changeset_link, + 'svn_repo-1|r123' => svn_changeset_link, + 'invalid|r123' => 'invalid|r123', + 'commit:hg1|abcd' => hg_changeset_link, + 'commit:invalid|abcd' => 'commit:invalid|abcd', + # source + 'source:some/file' => source_link, + 'source:hg1|some/file' => hg_source_link, + 'source:invalid|some/file' => 'source:invalid|some/file', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_cross_project_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'ecookbook:r2' => changeset_link, + 'ecookbook:svn1|r123' => svn_changeset_link, + 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123', + 'ecookbook:commit:hg1|abcd' => hg_changeset_link, + 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd', + 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd', + # source + 'ecookbook:source:some/file' => source_link, + 'ecookbook:source:hg1|some/file' => hg_source_link, + 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file', + 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file', + } + + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_git_commit + changeset_link = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:abcd' => changeset_link, + } + @project = Project.find(3) + r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => 'abcd', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'. + def test_redmine_links_darcs_commit + changeset_link = link_to('20080308225258-98289-abcd456efg.gz', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link, + } + @project = Project.find(3) + r = Repository::Darcs.create!( + :project => @project, :url => '/tmp/test/darcs', + :log_encoding => 'UTF-8') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '20080308225258-98289-abcd456efg.gz', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_redmine_links_mercurial_commit + changeset_link_rev = link_to('r123', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123' , + }, + :class => 'changeset', :title => 'test commit') + changeset_link_commit = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd' , + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'r123' => changeset_link_rev, + 'commit:abcd' => changeset_link_commit, + } + @project = Project.find(3) + r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_attachment_links + text = 'attachment:error281.txt' + result = link_to("error281.txt", "/attachments/download/1/error281.txt", + :class => "attachment") + assert_equal "

    #{result}

    ", + textilizable(text, + :attachments => Issue.find(3).attachments), + "#{text} failed" + end + + def test_attachment_link_should_link_to_latest_attachment + set_tmp_attachments_directory + a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago) + a2 = Attachment.generate!(:filename => "test.txt") + result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt", + :class => "attachment") + assert_equal "

    #{result}

    ", + textilizable('attachment:test.txt', :attachments => [a1, a2]) + end + + def test_wiki_links + russian_eacape = CGI.escape(@russian_test) + to_test = { + '[[CookBook documentation]]' => + link_to("CookBook documentation", + "/projects/ecookbook/wiki/CookBook_documentation", + :class => "wiki-page"), + '[[Another page|Page]]' => + link_to("Page", + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page"), + # title content should be formatted + '[[Another page|With _styled_ *title*]]' => + link_to("With styled title".html_safe, + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page"), + '[[Another page|With title containing HTML entities & markups]]' => + link_to("With title containing <strong>HTML entities & markups</strong>".html_safe, + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page"), + # link with anchor + '[[CookBook documentation#One-section]]' => + link_to("CookBook documentation", + "/projects/ecookbook/wiki/CookBook_documentation#One-section", + :class => "wiki-page"), + '[[Another page#anchor|Page]]' => + link_to("Page", + "/projects/ecookbook/wiki/Another_page#anchor", + :class => "wiki-page"), + # UTF8 anchor + "[[Another_page##{@russian_test}|#{@russian_test}]]" => + link_to(@russian_test, + "/projects/ecookbook/wiki/Another_page##{russian_eacape}", + :class => "wiki-page"), + # page that doesn't exist + '[[Unknown page]]' => + link_to("Unknown page", + "/projects/ecookbook/wiki/Unknown_page", + :class => "wiki-page new"), + '[[Unknown page|404]]' => + link_to("404", + "/projects/ecookbook/wiki/Unknown_page", + :class => "wiki-page new"), + # link to another project wiki + '[[onlinestore:]]' => + link_to("onlinestore", + "/projects/onlinestore/wiki", + :class => "wiki-page"), + '[[onlinestore:|Wiki]]' => + link_to("Wiki", + "/projects/onlinestore/wiki", + :class => "wiki-page"), + '[[onlinestore:Start page]]' => + link_to("Start page", + "/projects/onlinestore/wiki/Start_page", + :class => "wiki-page"), + '[[onlinestore:Start page|Text]]' => + link_to("Text", + "/projects/onlinestore/wiki/Start_page", + :class => "wiki-page"), + '[[onlinestore:Unknown page]]' => + link_to("Unknown page", + "/projects/onlinestore/wiki/Unknown_page", + :class => "wiki-page new"), + # striked through link + '-[[Another page|Page]]-' => + "".html_safe + + link_to("Page", + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page").html_safe + + "".html_safe, + '-[[Another page|Page]] link-' => + "".html_safe + + link_to("Page", + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page").html_safe + + " link".html_safe, + # escaping + '![[Another page|Page]]' => '[[Another page|Page]]', + # project does not exist + '[[unknowproject:Start]]' => '[[unknowproject:Start]]', + '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]', + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_wiki_links_within_local_file_generation_context + to_test = { + # link to a page + '[[CookBook documentation]]' => + link_to("CookBook documentation", "CookBook_documentation.html", + :class => "wiki-page"), + '[[CookBook documentation|documentation]]' => + link_to("documentation", "CookBook_documentation.html", + :class => "wiki-page"), + '[[CookBook documentation#One-section]]' => + link_to("CookBook documentation", "CookBook_documentation.html#One-section", + :class => "wiki-page"), + '[[CookBook documentation#One-section|documentation]]' => + link_to("documentation", "CookBook_documentation.html#One-section", + :class => "wiki-page"), + # page that doesn't exist + '[[Unknown page]]' => + link_to("Unknown page", "Unknown_page.html", + :class => "wiki-page new"), + '[[Unknown page|404]]' => + link_to("404", "Unknown_page.html", + :class => "wiki-page new"), + '[[Unknown page#anchor]]' => + link_to("Unknown page", "Unknown_page.html#anchor", + :class => "wiki-page new"), + '[[Unknown page#anchor|404]]' => + link_to("404", "Unknown_page.html#anchor", + :class => "wiki-page new"), + } + @project = Project.find(1) + to_test.each do |text, result| + assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :local) + end + end + + def test_wiki_links_within_wiki_page_context + page = WikiPage.find_by_title('Another_page' ) + to_test = { + '[[CookBook documentation]]' => + link_to("CookBook documentation", + "/projects/ecookbook/wiki/CookBook_documentation", + :class => "wiki-page"), + '[[CookBook documentation|documentation]]' => + link_to("documentation", + "/projects/ecookbook/wiki/CookBook_documentation", + :class => "wiki-page"), + '[[CookBook documentation#One-section]]' => + link_to("CookBook documentation", + "/projects/ecookbook/wiki/CookBook_documentation#One-section", + :class => "wiki-page"), + '[[CookBook documentation#One-section|documentation]]' => + link_to("documentation", + "/projects/ecookbook/wiki/CookBook_documentation#One-section", + :class => "wiki-page"), + # link to the current page + '[[Another page]]' => + link_to("Another page", + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page"), + '[[Another page|Page]]' => + link_to("Page", + "/projects/ecookbook/wiki/Another_page", + :class => "wiki-page"), + '[[Another page#anchor]]' => + link_to("Another page", + "#anchor", + :class => "wiki-page"), + '[[Another page#anchor|Page]]' => + link_to("Page", + "#anchor", + :class => "wiki-page"), + # page that doesn't exist + '[[Unknown page]]' => + link_to("Unknown page", + "/projects/ecookbook/wiki/Unknown_page?parent=Another_page", + :class => "wiki-page new"), + '[[Unknown page|404]]' => + link_to("404", + "/projects/ecookbook/wiki/Unknown_page?parent=Another_page", + :class => "wiki-page new"), + '[[Unknown page#anchor]]' => + link_to("Unknown page", + "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor", + :class => "wiki-page new"), + '[[Unknown page#anchor|404]]' => + link_to("404", + "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor", + :class => "wiki-page new"), + } + @project = Project.find(1) + to_test.each do |text, result| + assert_equal "

    #{result}

    ", + textilizable(WikiContent.new( :text => text, :page => page ), :text) + end + end + + def test_wiki_links_anchor_option_should_prepend_page_title_to_href + to_test = { + # link to a page + '[[CookBook documentation]]' => + link_to("CookBook documentation", + "#CookBook_documentation", + :class => "wiki-page"), + '[[CookBook documentation|documentation]]' => + link_to("documentation", + "#CookBook_documentation", + :class => "wiki-page"), + '[[CookBook documentation#One-section]]' => + link_to("CookBook documentation", + "#CookBook_documentation_One-section", + :class => "wiki-page"), + '[[CookBook documentation#One-section|documentation]]' => + link_to("documentation", + "#CookBook_documentation_One-section", + :class => "wiki-page"), + # page that doesn't exist + '[[Unknown page]]' => + link_to("Unknown page", + "#Unknown_page", + :class => "wiki-page new"), + '[[Unknown page|404]]' => + link_to("404", + "#Unknown_page", + :class => "wiki-page new"), + '[[Unknown page#anchor]]' => + link_to("Unknown page", + "#Unknown_page_anchor", + :class => "wiki-page new"), + '[[Unknown page#anchor|404]]' => + link_to("404", + "#Unknown_page_anchor", + :class => "wiki-page new"), + } + @project = Project.find(1) + to_test.each do |text, result| + assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :anchor) + end + end + + def test_html_tags + to_test = { + "
    content
    " => "

    <div>content</div>

    ", + "
    content
    " => "

    <div class=\"bold\">content</div>

    ", + "" => "

    <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 -->

    ", + " +

    <%= f.text_field :name, :required => true %>

    +

    <%= f.check_box :is_in_roadmap %>

    + +

    + + <% Tracker::CORE_FIELDS.each do |field| %> + + <% end %> +

    +<%= hidden_field_tag 'tracker[core_fields][]', '' %> + +<% if IssueCustomField.all.any? %> +

    + + <% IssueCustomField.all.each do |field| %> + + <% end %> +

    +<%= hidden_field_tag 'tracker[custom_field_ids][]', '' %> +<% end %> + +<% if @tracker.new_record? && @trackers.any? %> +

    +<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@trackers, :id, :name)) %>

    +<% end %> + + +<%= submit_tag l(@tracker.new_record? ? :button_create : :button_save) %> + + +
    +<% if @projects.any? %> +
    <%= l(:label_project_plural) %> +<%= render_project_nested_lists(@projects) do |p| + content_tag('label', check_box_tag('tracker[project_ids][]', p.id, @tracker.projects.to_a.include?(p), :id => nil) + ' ' + h(p)) +end %> +<%= hidden_field_tag('tracker[project_ids][]', '', :id => nil) %> +

    <%= check_all_links 'tracker_project_ids' %>

    +
    +<% end %> +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1b/1bfdad19f4f318cbdfbe4d8458bf52c9d4022d92.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1bfdad19f4f318cbdfbe4d8458bf52c9d4022d92.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,14 @@ +class AddRelationsPermissions < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.create :controller => "issue_relations", :action => "new", :description => "label_relation_new", :sort => 1080, :is_public => false, :mail_option => 0, :mail_enabled => 0 + Permission.create :controller => "issue_relations", :action => "destroy", :description => "label_relation_delete", :sort => 1085, :is_public => false, :mail_option => 0, :mail_enabled => 0 + end + + def self.down + Permission.where(:controller => "issue_relations", :action => "new").each {|p| p.destroy} + Permission.where(:controller => "issue_relations", :action => "destroy").each {|p| p.destroy} + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1c/1c28b35075b5e9d9a0869f79fc45dbaf27ac97ad.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1c28b35075b5e9d9a0869f79fc45dbaf27ac97ad.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,89 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'mime/types' + +module Redmine + module MimeType + + MIME_TYPES = { + 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade', + 'text/css' => 'css', + 'text/html' => 'html,htm,xhtml', + 'text/jsp' => 'jsp', + 'text/x-c' => 'c,cpp,cc,h,hh', + 'text/x-csharp' => 'cs', + 'text/x-java' => 'java', + 'text/x-html-template' => 'rhtml', + 'text/x-perl' => 'pl,pm', + 'text/x-php' => 'php,php3,php4,php5', + 'text/x-python' => 'py', + 'text/x-ruby' => 'rb,rbw,ruby,rake,erb', + 'text/x-csh' => 'csh', + 'text/x-sh' => 'sh', + 'text/xml' => 'xml,xsd,mxml', + 'text/yaml' => 'yml,yaml', + 'text/csv' => 'csv', + 'text/x-po' => 'po', + 'image/gif' => 'gif', + 'image/jpeg' => 'jpg,jpeg,jpe', + 'image/png' => 'png', + 'image/tiff' => 'tiff,tif', + 'image/x-ms-bmp' => 'bmp', + 'application/javascript' => 'js', + 'application/pdf' => 'pdf', + }.freeze + + EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)| + exts.split(',').each {|ext| map[ext.strip] = type} + map + end + + # returns mime type for name or nil if unknown + def self.of(name) + return nil unless name.present? + if m = name.to_s.match(/(^|\.)([^\.]+)$/) + extension = m[2].downcase + @known_types ||= Hash.new do |h, ext| + type = EXTENSIONS[ext] + type ||= MIME::Types.type_for(ext).first.to_s.presence + h[ext] = type + end + @known_types[extension] + end + end + + # Returns the css class associated to + # the mime type of name + def self.css_class_of(name) + mime = of(name) + mime && mime.gsub('/', '-') + end + + def self.main_mimetype_of(name) + mimetype = of(name) + mimetype.split('/').first if mimetype + end + + # return true if mime-type for name is type/* + # otherwise false + def self.is_type?(type, name) + main_mimetype = main_mimetype_of(name) + type.to_s == main_mimetype + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1c/1c38bd1806aa4d1bf50821953dfb345b483fc9ed.svn-base --- a/.svn/pristine/1c/1c38bd1806aa4d1bf50821953dfb345b483fc9ed.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 PatchesTest < ActiveSupport::TestCase - include Redmine::I18n - - context "ActiveRecord::Base.human_attribute_name" do - setup do - Setting.default_language = 'en' - end - - should "transform name to field_name" do - assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on') - end - - should "cut extra _id suffix for better validation" do - assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on_id') - end - - should "default to humanized value if no translation has been found (useful for custom fields)" do - assert_equal 'Patch name', ActiveRecord::Base.human_attribute_name('Patch name') - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1c/1c4e880a5c075a9e228087794274845b57f2a7a4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1c4e880a5c075a9e228087794274845b57f2a7a4.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,145 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueCategoriesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules, :issue_categories, + :issues + + def setup + User.current = nil + @request.session[:user_id] = 2 + end + + def test_new + @request.session[:user_id] = 2 # manager + get :new, :project_id => '1' + assert_response :success + assert_template 'new' + assert_select 'input[name=?]', 'issue_category[name]' + end + + def test_new_from_issue_form + @request.session[:user_id] = 2 # manager + xhr :get, :new, :project_id => '1' + + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_create + @request.session[:user_id] = 2 # manager + assert_difference 'IssueCategory.count' do + post :create, :project_id => '1', :issue_category => {:name => 'New category'} + end + assert_redirected_to '/projects/ecookbook/settings/categories' + category = IssueCategory.find_by_name('New category') + assert_not_nil category + assert_equal 1, category.project_id + end + + def test_create_failure + @request.session[:user_id] = 2 + post :create, :project_id => '1', :issue_category => {:name => ''} + assert_response :success + assert_template 'new' + end + + def test_create_from_issue_form + @request.session[:user_id] = 2 # manager + assert_difference 'IssueCategory.count' do + xhr :post, :create, :project_id => '1', :issue_category => {:name => 'New category'} + end + category = IssueCategory.order('id DESC').first + assert_equal 'New category', category.name + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + + def test_create_from_issue_form_with_failure + @request.session[:user_id] = 2 # manager + assert_no_difference 'IssueCategory.count' do + xhr :post, :create, :project_id => '1', :issue_category => {:name => ''} + end + + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_edit + @request.session[:user_id] = 2 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_select 'input[name=?][value=?]', 'issue_category[name]', 'Recipes' + end + + def test_update + assert_no_difference 'IssueCategory.count' do + put :update, :id => 2, :issue_category => { :name => 'Testing' } + end + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_equal 'Testing', IssueCategory.find(2).name + end + + def test_update_failure + put :update, :id => 2, :issue_category => { :name => '' } + assert_response :success + assert_template 'edit' + end + + def test_update_not_found + put :update, :id => 97, :issue_category => { :name => 'Testing' } + assert_response 404 + end + + def test_destroy_category_not_in_use + delete :destroy, :id => 2 + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_nil IssueCategory.find_by_id(2) + end + + def test_destroy_category_in_use + delete :destroy, :id => 1 + assert_response :success + assert_template 'destroy' + assert_not_nil IssueCategory.find_by_id(1) + end + + def test_destroy_category_in_use_with_reassignment + issue = Issue.where(:category_id => 1).first + delete :destroy, :id => 1, :todo => 'reassign', :reassign_to_id => 2 + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_nil IssueCategory.find_by_id(1) + # check that the issue was reassign + assert_equal 2, issue.reload.category_id + end + + def test_destroy_category_in_use_without_reassignment + issue = Issue.where(:category_id => 1).first + delete :destroy, :id => 1, :todo => 'nullify' + assert_redirected_to '/projects/ecookbook/settings/categories' + assert_nil IssueCategory.find_by_id(1) + # check that the issue category was nullified + assert_nil issue.reload.category_id + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1c/1c60d8632b51110591957f2fbcb2673042f354f8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1c60d8632b51110591957f2fbcb2673042f354f8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,99 @@ +source 'https://rubygems.org' + +gem "rails", "3.2.17" +gem "jquery-rails", "~> 2.0.2" +gem "coderay", "~> 1.1.0" +gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] +gem "builder", "3.0.0" + +# Optional gem for LDAP authentication +group :ldap do + gem "net-ldap", "~> 0.3.1" +end + +# Optional gem for OpenID authentication +group :openid do + gem "ruby-openid", "~> 2.3.0", :require => "openid" + gem "rack-openid" +end + +# Optional gem for exporting the gantt to a PNG file, not supported with jruby +platforms :mri, :mingw do + group :rmagick do + # RMagick 2 supports ruby 1.9 + # RMagick 1 would be fine for ruby 1.8 but Bundler does not support + # different requirements for the same gem on different platforms + gem "rmagick", ">= 2.0.0" + end +end + +platforms :jruby do + # jruby-openssl is bundled with JRuby 1.7.0 + gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0' + gem "activerecord-jdbc-adapter", "~> 1.3.2" +end + +# Include database gems for the adapters found in the database +# configuration file +require 'erb' +require 'yaml' +database_file = File.join(File.dirname(__FILE__), "config/database.yml") +if File.exist?(database_file) + database_config = YAML::load(ERB.new(IO.read(database_file)).result) + adapters = database_config.values.map {|c| c['adapter']}.compact.uniq + if adapters.any? + adapters.each do |adapter| + case adapter + when 'mysql2' + gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw] + gem "activerecord-jdbcmysql-adapter", :platforms => :jruby + when 'mysql' + gem "mysql", "~> 2.8.1", :platforms => [:mri, :mingw] + gem "activerecord-jdbcmysql-adapter", :platforms => :jruby + when /postgresql/ + gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw] + gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby + when /sqlite3/ + gem "sqlite3", :platforms => [:mri, :mingw] + gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby + when /sqlserver/ + gem "tiny_tds", "~> 0.5.1", :platforms => [:mri, :mingw] + gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw] + else + warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems") + end + end + else + warn("No adapter found in config/database.yml, please configure it first") + end +else + warn("Please configure your config/database.yml first") +end + +group :development do + gem "rdoc", ">= 2.4.2" + gem "yard" +end + +group :test do + gem "shoulda", "~> 3.3.2" + gem "mocha", ">= 0.14", :require => 'mocha/api' + if RUBY_VERSION >= '1.9.3' + gem "capybara", "~> 2.1.0" + gem "selenium-webdriver" + gem "database_cleaner" + end +end + +local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") +if File.exists?(local_gemfile) + puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(local_gemfile) +end + +# Load plugins' Gemfiles +Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file| + puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` + #TODO: switch to "eval_gemfile file" when bundler >= 1.2.0 will be required (rails 4) + instance_eval File.read(file), file +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1c/1ca8f284dbc1327ae70fe2ea5473249d28c671c0.svn-base --- a/.svn/pristine/1c/1ca8f284dbc1327ae70fe2ea5473249d28c671c0.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 NewsTest < ActiveSupport::TestCase - fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news - - def valid_news - { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) } - end - - def setup - end - - def test_create_should_send_email_notification - ActionMailer::Base.deliveries.clear - news = Project.find(1).news.new(valid_news) - - with_settings :notified_events => %w(news_added) do - assert news.save - end - assert_equal 1, ActionMailer::Base.deliveries.size - end - - def test_should_include_news_for_projects_with_news_enabled - project = projects(:projects_001) - assert project.enabled_modules.any?{ |em| em.name == 'news' } - - # News.latest should return news from projects_001 - assert News.latest.any? { |news| news.project == project } - end - - def test_should_not_include_news_for_projects_with_news_disabled - EnabledModule.delete_all(["project_id = ? AND name = ?", 2, 'news']) - project = Project.find(2) - - # Add a piece of news to the project - news = project.news.create(valid_news) - - # News.latest should not return that new piece of news - assert News.latest.include?(news) == false - end - - def test_should_only_include_news_from_projects_visibly_to_the_user - assert News.latest(User.anonymous).all? { |news| news.project.is_public? } - end - - def test_should_limit_the_amount_of_returned_news - # Make sure we have a bunch of news stories - 10.times { projects(:projects_001).news.create(valid_news) } - assert_equal 2, News.latest(users(:users_002), 2).size - assert_equal 6, News.latest(users(:users_002), 6).size - end - - def test_should_return_5_news_stories_by_default - # Make sure we have a bunch of news stories - 10.times { projects(:projects_001).news.create(valid_news) } - assert_equal 5, News.latest(users(:users_004)).size - end - - def test_attachments_should_be_visible - assert News.find(1).attachments_visible?(User.anonymous) - end - - def test_attachments_should_be_deletable_with_manage_news_permission - manager = User.find(2) - assert News.find(1).attachments_deletable?(manager) - end - - def test_attachments_should_not_be_deletable_without_manage_news_permission - manager = User.find(2) - Role.find_by_name('Manager').remove_permission!(:manage_news) - assert !News.find(1).attachments_deletable?(manager) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1c/1ce7c095bdee57845b24106ac69ceda35b831a10.svn-base --- a/.svn/pristine/1c/1ce7c095bdee57845b24106ac69ceda35b831a10.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -

    <%= link_to l(:label_user_plural), users_path %> » <%=l(:label_user_new)%>

    - -<%= labelled_form_for @user do |f| %> - <%= render :partial => 'form', :locals => { :f => f } %> - <% if email_delivery_enabled? %> -

    - <% end %> -

    - <%= submit_tag l(:button_create) %> - <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> -

    -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1d1a40d68d66d098f38161f4b90f0e887e1b73d9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d1a40d68d66d098f38161f4b90f0e887e1b73d9.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,15 @@ +class AddUniqueIndexOnTokensValue < ActiveRecord::Migration + def up + say_with_time "Adding unique index on tokens, this may take some time..." do + # Just in case + duplicates = Token.connection.select_values("SELECT value FROM #{Token.table_name} GROUP BY value HAVING COUNT(id) > 1") + Token.where(:value => duplicates).delete_all + + add_index :tokens, :value, :unique => true, :name => 'tokens_value' + end + end + + def down + remove_index :tokens, :name => 'tokens_value' + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1d23ef14041c621865426d5030ff2b1bdf3b4a55.svn-base --- a/.svn/pristine/1d/1d23ef14041c621865426d5030ff2b1bdf3b4a55.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,291 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'redmine/scm/adapters/abstract_adapter' -require 'uri' - -module Redmine - module Scm - module Adapters - class SubversionAdapter < AbstractAdapter - - # SVN executable name - SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn" - - class << self - def client_command - @@bin ||= SVN_BIN - end - - def sq_bin - @@sq_bin ||= shell_quote_command - end - - def client_version - @@client_version ||= (svn_binary_version || []) - end - - def client_available - # --xml options are introduced in 1.3. - # http://subversion.apache.org/docs/release-notes/1.3.html - client_version_above?([1, 3]) - end - - def svn_binary_version - scm_version = scm_version_from_command_line.dup - if scm_version.respond_to?(:force_encoding) - scm_version.force_encoding('ASCII-8BIT') - end - if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}) - m[2].scan(%r{\d+}).collect(&:to_i) - end - end - - def scm_version_from_command_line - shellout("#{sq_bin} --version") { |io| io.read }.to_s - end - end - - # Get info about the svn repository - def info - cmd = "#{self.class.sq_bin} info --xml #{target}" - cmd << credentials_string - info = nil - shellout(cmd) do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - doc = parse_xml(output) - # root_url = doc.elements["info/entry/repository/root"].text - info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'], - :lastrev => Revision.new({ - :identifier => doc['info']['entry']['commit']['revision'], - :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime, - :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "") - }) - }) - rescue - end - end - return nil if $? && $?.exitstatus != 0 - info - rescue CommandFailed - return nil - end - - # Returns an Entries collection - # or nil if the given path doesn't exist in the repository - def entries(path=nil, identifier=nil, options={}) - path ||= '' - identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" - entries = Entries.new - cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}" - cmd << credentials_string - shellout(cmd) do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - doc = parse_xml(output) - each_xml_element(doc['lists']['list'], 'entry') do |entry| - commit = entry['commit'] - commit_date = commit['date'] - # Skip directory if there is no commit date (usually that - # means that we don't have read access to it) - next if entry['kind'] == 'dir' && commit_date.nil? - name = entry['name']['__content__'] - entries << Entry.new({:name => URI.unescape(name), - :path => ((path.empty? ? "" : "#{path}/") + name), - :kind => entry['kind'], - :size => ((s = entry['size']) ? s['__content__'].to_i : nil), - :lastrev => Revision.new({ - :identifier => commit['revision'], - :time => Time.parse(commit_date['__content__'].to_s).localtime, - :author => ((a = commit['author']) ? a['__content__'] : nil) - }) - }) - end - rescue Exception => e - logger.error("Error parsing svn output: #{e.message}") - logger.error("Output was:\n #{output}") - end - end - return nil if $? && $?.exitstatus != 0 - logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? - entries.sort_by_name - end - - def properties(path, identifier=nil) - # proplist xml output supported in svn 1.5.0 and higher - return nil unless self.class.client_version_above?([1, 5, 0]) - - identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" - cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}" - cmd << credentials_string - properties = {} - shellout(cmd) do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - doc = parse_xml(output) - each_xml_element(doc['properties']['target'], 'property') do |property| - properties[ property['name'] ] = property['__content__'].to_s - end - rescue - end - end - return nil if $? && $?.exitstatus != 0 - properties - end - - def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) - path ||= '' - identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" - identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1 - revisions = Revisions.new - cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}" - cmd << credentials_string - cmd << " --verbose " if options[:with_paths] - cmd << " --limit #{options[:limit].to_i}" if options[:limit] - cmd << ' ' + target(path) - shellout(cmd) do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - doc = parse_xml(output) - each_xml_element(doc['log'], 'logentry') do |logentry| - paths = [] - each_xml_element(logentry['paths'], 'path') do |path| - paths << {:action => path['action'], - :path => path['__content__'], - :from_path => path['copyfrom-path'], - :from_revision => path['copyfrom-rev'] - } - end if logentry['paths'] && logentry['paths']['path'] - paths.sort! { |x,y| x[:path] <=> y[:path] } - - revisions << Revision.new({:identifier => logentry['revision'], - :author => (logentry['author'] ? logentry['author']['__content__'] : ""), - :time => Time.parse(logentry['date']['__content__'].to_s).localtime, - :message => logentry['msg']['__content__'], - :paths => paths - }) - end - rescue - end - end - return nil if $? && $?.exitstatus != 0 - revisions - end - - def diff(path, identifier_from, identifier_to=nil) - path ||= '' - identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : '' - - identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) - - cmd = "#{self.class.sq_bin} diff -r " - cmd << "#{identifier_to}:" - cmd << "#{identifier_from}" - cmd << " #{target(path)}@#{identifier_from}" - cmd << credentials_string - diff = [] - shellout(cmd) do |io| - io.each_line do |line| - diff << line - end - end - return nil if $? && $?.exitstatus != 0 - diff - end - - def cat(path, identifier=nil) - identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" - cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}" - cmd << credentials_string - cat = nil - shellout(cmd) do |io| - io.binmode - cat = io.read - end - return nil if $? && $?.exitstatus != 0 - cat - end - - def annotate(path, identifier=nil) - identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" - cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}" - cmd << credentials_string - blame = Annotate.new - shellout(cmd) do |io| - io.each_line do |line| - next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} - rev = $1 - blame.add_line($3.rstrip, - Revision.new( - :identifier => rev, - :revision => rev, - :author => $2.strip - )) - end - end - return nil if $? && $?.exitstatus != 0 - blame - end - - private - - def credentials_string - str = '' - str << " --username #{shell_quote(@login)}" unless @login.blank? - str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank? - str << " --no-auth-cache --non-interactive" - str - end - - # Helper that iterates over the child elements of a xml node - # MiniXml returns a hash when a single child is found - # or an array of hashes for multiple children - def each_xml_element(node, name) - if node && node[name] - if node[name].is_a?(Hash) - yield node[name] - else - node[name].each do |element| - yield element - end - end - end - end - - def target(path = '') - base = path.match(/^\//) ? root_url : url - uri = "#{base}/#{path}" - uri = URI.escape(URI.escape(uri), '[]') - shell_quote(uri.gsub(/[?<>\*]/, '')) - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1d4c356684e7bc2f59df7e93707ab136f908ac26.svn-base --- a/.svn/pristine/1d/1d4c356684e7bc2f59df7e93707ab136f908ac26.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1d70cf0be89de5fcd0a7d79fde026912fb2a0dcb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d70cf0be89de5fcd0a7d79fde026912fb2a0dcb.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1104 @@ +# Serbian translations for Redmine +# by Vladimir Medarović (vlada@medarovic.com) +sr: + direction: ltr + 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: "%d.%m.%Y." + short: "%e %b" + long: "%B %e, %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: + - :day + - :month + - :year + + time: + formats: + default: "%d.%m.%Y. у %H:%M" + time: "%H:%M" + short: "%d. %b у %H:%M" + long: "%d. %B %Y у %H:%M" + am: "am" + pm: "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: "Ñкоро годину дана" + other: "Ñкоро %{count} год." + + number: + 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: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + 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: 'Serbian Cyrillic (СрпÑки)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + 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_email_sent: "E-порука је поÑлата на %{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: ÐžÐ´Ð½Ð¾Ñ Ñ€ÐµÑˆÐµÐ½Ð¸Ñ… проблема је ажуриран. + + 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: "Wiki Ñтраница '%{id}' је додата" + mail_body_wiki_content_added: "%{author} је додао wiki Ñтраницу '%{id}'." + mail_subject_wiki_content_updated: "Wiki Ñтраница '%{id}' је ажурирана" + mail_body_wiki_content_updated: "%{author} је ажурирао wiki Ñтраницу '%{id}'." + + + 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: 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_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: Матични задатак + + 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: Ограничење Ñадржаја извора веÑти + setting_default_projects_public: Подразумева Ñе јавно приказивање нових пројеката + setting_autofetch_changesets: Извршавање аутоматÑког преузимања + setting_sys_api_enabled: Омогућавање WS за управљање Ñпремиштем + 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_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: Омогућавање WS долазне е-поруке + 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 web уÑлуге + setting_cache_formatted_text: Кеширање обрађеног текÑта + + 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: Форуми + + label_user: КориÑник + label_user_plural: КориÑници + label_user_new: Ðови кориÑник + label_user_anonymous: Ðнониман + label_project: Пројекат + label_project_new: Ðови пројекат + label_project_plural: Пројекти + label_x_projects: + zero: нема пројеката + one: један пројекат + 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: 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: један коментар + 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: Извори веÑти + 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_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_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: ГенериÑање кључа + 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: Пошаљи е-поруку Ñа обавештењем приликом копирања пројекта + + 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_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: "%{author} је пријавио проблем %{id}." + text_issue_updated: "%{author} је ажурирао проблем %{id}." + 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_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КориÑници Ñа иÑтим 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: "Ðакон уклањања појединих или Ñвих ваших дозвола нећете више моћи да уређујете овај пројекат.\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: СиÑтемÑка активноÑÑ‚ + + field_time_entries: Време евиденције + project_module_gantt: Гантов дијаграм + project_module_calendar: Календар + + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: Ñви + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Са потпројектима + label_cross_project_tree: Са Ñтаблом пројекта + label_cross_project_hierarchy: Са хијерархијом пројекта + label_cross_project_system: Са Ñвим пројектима + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Укупно + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1d734add023a98369ca3ca65fe3091fb8e56d827.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d734add023a98369ca3ca65fe3091fb8e56d827.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,193 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Journal < ActiveRecord::Base + belongs_to :journalized, :polymorphic => true + # added as a quick fix to allow eager loading of the polymorphic association + # since always associated to an issue, for now + belongs_to :issue, :foreign_key => :journalized_id + + belongs_to :user + has_many :details, :class_name => "JournalDetail", :dependent => :delete_all + attr_accessor :indice + + acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, + :description => :notes, + :author => :user, + :group => :issue, + :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} + + acts_as_activity_provider :type => 'issues', + :author_key => :user_id, + :find_options => {:include => [{:issue => :project}, :details, :user], + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + + " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} + + before_create :split_private_notes + after_create :send_notification + + scope :visible, lambda {|*args| + user = args.shift || User.current + + includes(:issue => :project). + where(Issue.visible_condition(user, *args)). + where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) + } + + def save(*args) + # Do not save an empty journal + (details.empty? && notes.blank?) ? false : super + end + + # Returns journal details that are visible to user + def visible_details(user=User.current) + details.select do |detail| + if detail.property == 'cf' + detail.custom_field && detail.custom_field.visible_by?(project, user) + elsif detail.property == 'relation' + Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user) + else + true + end + end + end + + def each_notification(users, &block) + if users.any? + users_by_details_visibility = users.group_by do |user| + visible_details(user) + end + users_by_details_visibility.each do |visible_details, users| + if notes? || visible_details.any? + yield(users) + end + end + end + end + + # Returns the new status if the journal contains a status change, otherwise nil + def new_status + c = details.detect {|detail| detail.prop_key == 'status_id'} + (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil + end + + def new_value_for(prop) + c = details.detect {|detail| detail.prop_key == prop} + c ? c.value : nil + end + + def editable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) + end + + def project + journalized.respond_to?(:project) ? journalized.project : nil + end + + def attachments + journalized.respond_to?(:attachments) ? journalized.attachments : nil + end + + # Returns a string of css classes + def css_classes + s = 'journal' + s << ' has-notes' unless notes.blank? + s << ' has-details' unless details.blank? + s << ' private-notes' if private_notes? + s + end + + def notify? + @notify != false + end + + def notify=(arg) + @notify = arg + end + + def notified_users + notified = journalized.notified_users + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified + end + + def recipients + notified_users.map(&:mail) + end + + def notified_watchers + notified = journalized.notified_watchers + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified + end + + def watcher_recipients + notified_watchers.map(&:mail) + end + + # Sets @custom_field instance variable on journals details using a single query + def self.preload_journals_details_custom_fields(journals) + field_ids = journals.map(&:details).flatten.select {|d| d.property == 'cf'}.map(&:prop_key).uniq + if field_ids.any? + fields_by_id = CustomField.find_all_by_id(field_ids).inject({}) {|h, f| h[f.id] = f; h} + journals.each do |journal| + journal.details.each do |detail| + if detail.property == 'cf' + detail.instance_variable_set "@custom_field", fields_by_id[detail.prop_key.to_i] + end + end + end + end + journals + end + + private + + def split_private_notes + if private_notes? + if notes.present? + if details.any? + # Split the journal (notes/changes) so we don't have half-private journals + journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) + journal.details = details + journal.save + self.details = [] + self.created_on = journal.created_on + end + else + # Blank notes should not be private + self.private_notes = false + end + end + true + end + + def send_notification + if notify? && (Setting.notified_events.include?('issue_updated') || + (Setting.notified_events.include?('issue_note_added') && notes.present?) || + (Setting.notified_events.include?('issue_status_updated') && new_status.present?) || + (Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?) + ) + Mailer.deliver_issue_edit(self) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1da882d06ce8605d4176f84b3f42ce44acc55377.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1da882d06ce8605d4176f84b3f42ce44acc55377.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1212 @@ +# Russian localization for Ruby on Rails 2.2+ +# by Yaroslav Markin +# +# Be sure to check out "russian" gem (http://github.com/yaroslav/russian) for +# full Russian language support in Rails (month names, pluralization, etc). +# The following is an excerpt from that gem. +# +# Ð”Ð»Ñ Ð¿Ð¾Ð»Ð½Ð¾Ñ†ÐµÐ½Ð½Ð¾Ð¹ поддержки руÑÑкого Ñзыка (варианты названий меÑÑцев, +# Ð¿Ð»ÑŽÑ€Ð°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¸ так далее) в Rails 2.2 нужно иÑпользовать gem "russian" +# (http://github.com/yaroslav/russian). Следующие данные -- выдержка их него, чтобы +# была возможноÑть минимальной локализации Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½Ð° руÑÑкий Ñзык. + +ru: + direction: ltr + date: + formats: + default: "%d.%m.%Y" + short: "%d %b" + long: "%d %B %Y" + + day_names: [воÑкреÑенье, понедельник, вторник, Ñреда, четверг, пÑтница, Ñуббота] + standalone_day_names: [ВоÑкреÑенье, Понедельник, Вторник, Среда, Четверг, ПÑтница, Суббота] + abbr_day_names: [Ð’Ñ, Пн, Ð’Ñ‚, Ср, Чт, Пт, Сб] + + month_names: [~, ÑнварÑ, февралÑ, марта, апрелÑ, маÑ, июнÑ, июлÑ, авгуÑта, ÑентÑбрÑ, октÑбрÑ, ноÑбрÑ, декабрÑ] + # see russian gem for info on "standalone" day names + standalone_month_names: [~, Январь, Февраль, Март, Ðпрель, Май, Июнь, Июль, ÐвгуÑÑ‚, СентÑбрь, ОктÑбрь, ÐоÑбрь, Декабрь] + abbr_month_names: [~, Ñнв., февр., марта, апр., маÑ, июнÑ, июлÑ, авг., Ñент., окт., ноÑб., дек.] + standalone_abbr_month_names: [~, Ñнв., февр., март, апр., май, июнь, июль, авг., Ñент., окт., ноÑб., дек.] + + order: + - :day + - :month + - :year + + time: + formats: + default: "%a, %d %b %Y, %H:%M:%S %z" + time: "%H:%M" + short: "%d %b, %H:%M" + long: "%d %B %Y, %H:%M" + + am: "утра" + pm: "вечера" + + number: + format: + separator: "," + delimiter: " " + precision: 3 + + currency: + format: + format: "%n %u" + unit: "руб." + separator: "." + delimiter: " " + precision: 2 + + percentage: + format: + delimiter: "" + + precision: + format: + delimiter: "" + + human: + format: + delimiter: "" + precision: 3 + # Rails 2.2 + # storage_units: [байт, КБ, МБ, ГБ, ТБ] + + # Rails 2.3 + storage_units: + # Storage units output formatting. + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u" + units: + byte: + one: "байт" + few: "байта" + many: "байт" + other: "байта" + kb: "КБ" + mb: "МБ" + gb: "ГБ" + tb: "ТБ" + + datetime: + distance_in_words: + half_a_minute: "меньше минуты" + less_than_x_seconds: + one: "меньше %{count} Ñекунды" + few: "меньше %{count} Ñекунд" + many: "меньше %{count} Ñекунд" + other: "меньше %{count} Ñекунды" + x_seconds: + one: "%{count} Ñекунда" + few: "%{count} Ñекунды" + many: "%{count} Ñекунд" + other: "%{count} Ñекунды" + less_than_x_minutes: + one: "меньше %{count} минуты" + few: "меньше %{count} минут" + many: "меньше %{count} минут" + other: "меньше %{count} минуты" + x_minutes: + one: "%{count} минуту" + few: "%{count} минуты" + many: "%{count} минут" + other: "%{count} минуты" + about_x_hours: + one: "около %{count} чаÑа" + few: "около %{count} чаÑов" + many: "около %{count} чаÑов" + other: "около %{count} чаÑа" + x_hours: + one: "%{count} чаÑ" + few: "%{count} чаÑа" + many: "%{count} чаÑов" + other: "%{count} чаÑа" + x_days: + one: "%{count} день" + few: "%{count} днÑ" + many: "%{count} дней" + other: "%{count} днÑ" + about_x_months: + one: "около %{count} меÑÑца" + few: "около %{count} меÑÑцев" + many: "около %{count} меÑÑцев" + other: "около %{count} меÑÑца" + x_months: + one: "%{count} меÑÑц" + few: "%{count} меÑÑца" + many: "%{count} меÑÑцев" + other: "%{count} меÑÑца" + about_x_years: + one: "около %{count} года" + few: "около %{count} лет" + many: "около %{count} лет" + other: "около %{count} лет" + over_x_years: + one: "больше %{count} года" + few: "больше %{count} лет" + many: "больше %{count} лет" + other: "больше %{count} лет" + almost_x_years: + one: "почти %{count} год" + few: "почти %{count} года" + many: "почти %{count} лет" + other: "почти %{count} года" + prompts: + year: "Год" + month: "МеÑÑц" + day: "День" + hour: "ЧаÑов" + minute: "Минут" + second: "Секунд" + + activerecord: + errors: + template: + header: + one: "%{model}: Ñохранение не удалоÑÑŒ из-за %{count} ошибки" + few: "%{model}: Ñохранение не удалоÑÑŒ из-за %{count} ошибок" + many: "%{model}: Ñохранение не удалоÑÑŒ из-за %{count} ошибок" + other: "%{model}: Ñохранение не удалоÑÑŒ из-за %{count} ошибки" + + body: "Проблемы возникли Ñо Ñледующими полÑми:" + + messages: + inclusion: "имеет непредуÑмотренное значение" + exclusion: "имеет зарезервированное значение" + invalid: "имеет неверное значение" + confirmation: "не Ñовпадает Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸ÐµÐ¼" + accepted: "нужно подтвердить" + empty: "не может быть пуÑтым" + blank: "не может быть пуÑтым" + too_long: + one: "Ñлишком большой длины (не может быть больше чем %{count} Ñимвол)" + few: "Ñлишком большой длины (не может быть больше чем %{count} Ñимвола)" + many: "Ñлишком большой длины (не может быть больше чем %{count} Ñимволов)" + other: "Ñлишком большой длины (не может быть больше чем %{count} Ñимвола)" + too_short: + one: "недоÑтаточной длины (не может быть меньше %{count} Ñимвола)" + few: "недоÑтаточной длины (не может быть меньше %{count} Ñимволов)" + many: "недоÑтаточной длины (не может быть меньше %{count} Ñимволов)" + other: "недоÑтаточной длины (не может быть меньше %{count} Ñимвола)" + wrong_length: + one: "неверной длины (может быть длиной ровно %{count} Ñимвол)" + few: "неверной длины (может быть длиной ровно %{count} Ñимвола)" + many: "неверной длины (может быть длиной ровно %{count} Ñимволов)" + other: "неверной длины (может быть длиной ровно %{count} Ñимвола)" + taken: "уже ÑущеÑтвует" + not_a_number: "не ÑвлÑетÑÑ Ñ‡Ð¸Ñлом" + 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" + + support: + array: + # Rails 2.2 + sentence_connector: "и" + skip_last_comma: true + + # Rails 2.3 + words_connector: ", " + two_words_connector: " и " + last_word_connector: " и " + + actionview_instancetag_blank_option: Выберите + + button_activate: Ðктивировать + button_add: Добавить + button_annotate: ÐвторÑтво + button_apply: Применить + button_archive: Ðрхивировать + button_back: Ðазад + button_cancel: Отмена + button_change_password: Изменить пароль + button_change: Изменить + button_check_all: Отметить вÑе + button_clear: ОчиÑтить + button_configure: Параметры + button_copy: Копировать + button_create: Создать + button_create_and_continue: Создать и продолжить + button_delete: Удалить + button_download: Загрузить + button_edit: Редактировать + button_edit_associated_wikipage: "Редактировать ÑвÑзанную wiki-Ñтраницу: %{page_title}" + button_list: СпиÑок + button_lock: Заблокировать + button_login: Вход + button_log_time: Затраченное Ð²Ñ€ÐµÐ¼Ñ + button_move: ПеремеÑтить + button_quote: Цитировать + button_rename: Переименовать + button_reply: Ответить + button_reset: СброÑить + button_rollback: ВернутьÑÑ Ðº данной верÑии + button_save: Сохранить + button_sort: Сортировать + button_submit: ПринÑть + button_test: Проверить + button_unarchive: Разархивировать + button_uncheck_all: ОчиÑтить + button_unlock: Разблокировать + button_unwatch: Ðе Ñледить + button_update: Обновить + button_view: ПроÑмотреть + button_watch: Следить + + default_activity_design: Проектирование + default_activity_development: Разработка + default_doc_category_tech: ТехничеÑÐºÐ°Ñ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ + default_doc_category_user: ПользовательÑÐºÐ°Ñ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ + default_issue_status_in_progress: Ð’ работе + default_issue_status_closed: Закрыта + default_issue_status_feedback: ÐžÐ±Ñ€Ð°Ñ‚Ð½Ð°Ñ ÑвÑзь + default_issue_status_new: ÐÐ¾Ð²Ð°Ñ + default_issue_status_rejected: Отклонена + default_issue_status_resolved: Решена + default_priority_high: Ð’Ñ‹Ñокий + default_priority_immediate: Ðемедленный + default_priority_low: Ðизкий + default_priority_normal: Ðормальный + default_priority_urgent: Срочный + default_role_developer: Разработчик + default_role_manager: Менеджер + default_role_reporter: Репортёр + default_tracker_bug: Ошибка + default_tracker_feature: Улучшение + default_tracker_support: Поддержка + + enumeration_activities: ДейÑÑ‚Ð²Ð¸Ñ (учёт времени) + enumeration_doc_categories: Категории документов + enumeration_issue_priorities: Приоритеты задач + + error_can_not_remove_role: Эта роль иÑпользуетÑÑ Ð¸ не может быть удалена. + error_can_not_delete_custom_field: Ðевозможно удалить наÑтраиваемое поле + error_can_not_delete_tracker: Этот трекер Ñодержит задачи и не может быть удален. + error_can_t_load_default_data: "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¿Ð¾ умолчанию не была загружена: %{value}" + error_issue_not_found_in_project: Задача не была найдена или не прикреплена к Ñтому проекту + error_scm_annotate: "Данные отÑутÑтвуют или не могут быть подпиÑаны." + error_scm_command_failed: "Ошибка доÑтупа к хранилищу: %{value}" + error_scm_not_found: Хранилище не Ñодержит запиÑи и/или иÑправлениÑ. + error_unable_to_connect: Ðевозможно подключитьÑÑ (%{value}) + error_unable_delete_issue_status: Ðевозможно удалить ÑÑ‚Ð°Ñ‚ÑƒÑ Ð·Ð°Ð´Ð°Ñ‡Ð¸ + + field_account: Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ + field_activity: ДеÑтельноÑть + field_admin: ÐдминиÑтратор + field_assignable: Задача может быть назначена Ñтой роли + field_assigned_to: Ðазначена + field_attr_firstname: Ð˜Ð¼Ñ + field_attr_lastname: Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ + field_attr_login: Ðтрибут Login + field_attr_mail: email + field_author: Ðвтор + field_auth_source: Режим аутентификации + field_base_dn: BaseDN + field_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + field_column_names: Столбцы + field_comments: Комментарий + field_comments_sorting: Отображение комментариев + field_content: Content + field_created_on: Создано + field_default_value: Значение по умолчанию + field_delay: Отложить + field_description: ОпиÑание + field_done_ratio: ГотовноÑть + field_downloads: Загрузки + field_due_date: Дата Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ + field_editable: Редактируемое + field_effective_date: Дата + field_estimated_hours: Оценка времени + field_field_format: Формат + field_filename: Файл + field_filesize: Размер + field_firstname: Ð˜Ð¼Ñ + field_fixed_version: ВерÑÐ¸Ñ + field_hide_mail: Скрывать мой email + field_homepage: Ð¡Ñ‚Ð°Ñ€Ñ‚Ð¾Ð²Ð°Ñ Ñтраница + field_host: Компьютер + field_hours: чаÑ(а,ов) + field_identifier: Уникальный идентификатор + field_identity_url: OpenID URL + field_is_closed: Задача закрыта + field_is_default: Значение по умолчанию + field_is_filter: ИÑпользуетÑÑ Ð² качеÑтве фильтра + field_is_for_all: Ð”Ð»Ñ Ð²Ñех проектов + field_is_in_roadmap: Задачи, отображаемые в оперативном плане + field_is_public: ОбщедоÑтупный + field_is_required: ОбÑзательное + field_issue_to: СвÑзанные задачи + field_issue: Задача + field_language: Язык + field_last_login_on: ПоÑледнее подключение + field_lastname: Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ + field_login: Пользователь + field_mail: Email + field_mail_notification: Ð£Ð²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ email + field_max_length: МакÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° + field_min_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° + field_name: Ð˜Ð¼Ñ + field_new_password: Ðовый пароль + field_notes: ÐŸÑ€Ð¸Ð¼ÐµÑ‡Ð°Ð½Ð¸Ñ + field_onthefly: Создание Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð½Ð° лету + field_parent_title: РодительÑÐºÐ°Ñ Ñтраница + field_parent: РодительÑкий проект + field_parent_issue: РодительÑÐºÐ°Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° + field_password_confirmation: Подтверждение + field_password: Пароль + field_port: Порт + field_possible_values: Возможные Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ + field_priority: Приоритет + field_project: Проект + field_redirect_existing_links: Перенаправить ÑущеÑтвующие ÑÑылки + field_regexp: РегулÑрное выражение + field_role: Роль + field_searchable: ДоÑтупно Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñка + field_spent_on: Дата + field_start_date: Ðачата + field_start_page: Ð¡Ñ‚Ð°Ñ€Ñ‚Ð¾Ð²Ð°Ñ Ñтраница + field_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ + field_subject: Тема + field_subproject: Подпроект + field_summary: Краткое опиÑание + field_text: ТекÑтовое поле + field_time_entries: Затраченное Ð²Ñ€ÐµÐ¼Ñ + field_time_zone: ЧаÑовой поÑÑ + field_title: Заголовок + field_tracker: Трекер + field_type: Тип + field_updated_on: Обновлено + field_url: URL + field_user: Пользователь + field_value: Значение + field_version: ВерÑÐ¸Ñ + field_watcher: Ðаблюдатель + + general_csv_decimal_separator: ',' + general_csv_encoding: UTF-8 + general_csv_separator: ';' + general_first_day_of_week: '1' + general_lang_name: 'Russian (РуÑÑкий)' + general_pdf_encoding: UTF-8 + general_text_no: 'нет' + general_text_No: 'Ðет' + general_text_yes: 'да' + general_text_Yes: 'Да' + + label_activity: ДейÑÑ‚Ð²Ð¸Ñ + label_add_another_file: Добавить ещё один файл + label_added_time_by: "Добавил(а) %{author} %{age} назад" + label_added: добавлено + label_add_note: Добавить замечание + label_administration: ÐдминиÑтрирование + label_age: ВозраÑÑ‚ + label_ago: дней(Ñ) назад + label_all_time: вÑÑ‘ Ð²Ñ€ÐµÐ¼Ñ + label_all_words: Ð’Ñе Ñлова + label_all: вÑе + label_and_its_subprojects: "%{value} и вÑе подпроекты" + label_applied_status: Применимый ÑÑ‚Ð°Ñ‚ÑƒÑ + label_ascending: По возраÑтанию + label_assigned_to_me_issues: Мои задачи + label_associated_revisions: СвÑзанные редакции + label_attachment: Файл + label_attachment_delete: Удалить файл + label_attachment_new: Ðовый файл + label_attachment_plural: Файлы + label_attribute: Ðтрибут + label_attribute_plural: Ðтрибуты + label_authentication: ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ + label_auth_source: Режим аутентификации + label_auth_source_new: Ðовый режим аутентификации + label_auth_source_plural: Режимы аутентификации + label_blocked_by: блокируетÑÑ + label_blocks: блокирует + label_board: Форум + label_board_new: Ðовый форум + label_board_plural: Форумы + label_boolean: ЛогичеÑкий + label_browse: Обзор + label_bulk_edit_selected_issues: Редактировать вÑе выбранные задачи + label_calendar: Календарь + label_calendar_filter: Ð’ÐºÐ»ÑŽÑ‡Ð°Ñ + label_calendar_no_assigned: не мои + label_change_plural: Правки + label_change_properties: Изменить ÑвойÑтва + label_change_status: Изменить ÑÑ‚Ð°Ñ‚ÑƒÑ + label_change_view_all: ПроÑмотреть вÑе Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ + label_changes_details: ПодробноÑти по вÑем изменениÑм + label_changeset_plural: Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ + label_chronological_order: Ð’ хронологичеÑком порÑдке + label_closed_issues: закрыто + label_closed_issues_plural: закрыто + label_closed_issues_plural2: закрыто + label_closed_issues_plural5: закрыто + label_comment: комментарий + label_comment_add: ОÑтавить комментарий + label_comment_added: Добавленный комментарий + label_comment_delete: Удалить комментарии + label_comment_plural: Комментарии + label_comment_plural2: ÐºÐ¾Ð¼Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ð¸Ñ + label_comment_plural5: комментариев + label_commits_per_author: Изменений на Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ + label_commits_per_month: Изменений в меÑÑц + label_confirmation: Подтверждение + label_contains: Ñодержит + label_copied: Ñкопировано + label_copy_workflow_from: Скопировать поÑледовательноÑть дейÑтвий из + label_current_status: Текущий ÑÑ‚Ð°Ñ‚ÑƒÑ + label_current_version: Ð¢ÐµÐºÑƒÑ‰Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ + label_custom_field: ÐаÑтраиваемое поле + label_custom_field_new: Ðовое наÑтраиваемое поле + label_custom_field_plural: ÐаÑтраиваемые Ð¿Ð¾Ð»Ñ + label_date_from: С + label_date_from_to: С %{start} по %{end} + label_date_range: временной интервал + label_date_to: по + label_date: Дата + label_day_plural: дней(Ñ) + label_default: По умолчанию + label_default_columns: Столбцы по умолчанию + label_deleted: удалено + label_descending: По убыванию + label_details: ПодробноÑти + label_diff_inline: в текÑте + label_diff_side_by_side: Ñ€Ñдом + label_disabled: отключено + label_display: Отображение + label_display_per_page: "Ðа Ñтраницу: %{value}" + label_document: Документ + label_document_added: Добавлен документ + label_document_new: Ðовый документ + label_document_plural: Документы + label_downloads_abbr: Скачиваний + label_duplicated_by: дублируетÑÑ + label_duplicates: дублирует + label_end_to_end: Ñ ÐºÐ¾Ð½Ñ†Ð° к концу + label_end_to_start: Ñ ÐºÐ¾Ð½Ñ†Ð° к началу + label_enumeration_new: Ðовое значение + label_enumerations: СпиÑки значений + label_environment: Окружение + label_equals: ÑоответÑтвует + label_example: Пример + label_export_to: ЭкÑпортировать в + label_feed_plural: Atom + label_feeds_access_key_created_on: "Ключ доÑтупа Atom Ñоздан %{value} назад" + label_f_hour: "%{value} чаÑ" + label_f_hour_plural: "%{value} чаÑов" + label_file_added: Добавлен файл + label_file_plural: Файлы + label_filter_add: Добавить фильтр + label_filter_plural: Фильтры + label_float: С плавающей точкой + label_follows: Ð¿Ñ€ÐµÐ´Ñ‹Ð´ÑƒÑ‰Ð°Ñ + label_gantt: Диаграмма Ганта + label_general: Общее + label_generate_key: Сгенерировать ключ + label_greater_or_equal: ">=" + label_help: Помощь + label_history: ИÑÑ‚Ð¾Ñ€Ð¸Ñ + label_home: ДомашнÑÑ Ñтраница + label_incoming_emails: Приём Ñообщений + label_index_by_date: ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ñтраниц + label_index_by_title: Оглавление + label_information_plural: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + label_information: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + label_in_less_than: менее чем + label_in_more_than: более чем + label_integer: Целый + label_internal: Внутренний + label_in: в + label_issue: Задача + label_issue_added: Добавлена задача + label_issue_category_new: ÐÐ¾Ð²Ð°Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + label_issue_category_plural: Категории задачи + label_issue_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ + label_issue_new: ÐÐ¾Ð²Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° + label_issue_plural: Задачи + label_issues_by: "Сортировать по %{value}" + label_issue_status_new: Ðовый ÑÑ‚Ð°Ñ‚ÑƒÑ + label_issue_status_plural: СтатуÑÑ‹ задач + label_issue_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð·Ð°Ð´Ð°Ñ‡Ð¸ + label_issue_tracking: Задачи + label_issue_updated: Обновлена задача + label_issue_view_all: ПроÑмотреть вÑе задачи + label_issue_watchers: Ðаблюдатели + label_jump_to_a_project: Перейти к проекту... + label_language_based: Ðа оÑнове Ñзыка + label_last_changes: "менее %{count} изменений" + label_last_login: ПоÑледнее подключение + label_last_month: поÑледний меÑÑц + label_last_n_days: "поÑледние %{count} дней" + label_last_week: поÑледнÑÑ Ð½ÐµÐ´ÐµÐ»Ñ + label_latest_revision: ПоÑледнÑÑ Ñ€ÐµÐ´Ð°ÐºÑ†Ð¸Ñ + label_latest_revision_plural: ПоÑледние редакции + label_ldap_authentication: ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ LDAP + label_less_or_equal: <= + label_less_than_ago: менее, чем дней(Ñ) назад + label_list: СпиÑок + label_loading: Загрузка... + label_logged_as: Вошли как + label_login: Войти + label_login_with_open_id_option: или войти Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ OpenID + label_logout: Выйти + label_max_size: МакÑимальный размер + label_member_new: Ðовый учаÑтник + label_member: УчаÑтник + label_member_plural: УчаÑтники + label_message_last: ПоÑледнее Ñообщение + label_message_new: Ðовое Ñообщение + label_message_plural: Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + label_message_posted: Добавлено Ñообщение + label_me: мне + label_min_max_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ - макÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° + label_modified: изменено + label_module_plural: Модули + label_months_from: меÑÑцев(ца) Ñ + label_month: МеÑÑц + label_more_than_ago: более, чем дней(Ñ) назад + label_more: Больше + label_my_account: ÐœÐ¾Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ + label_my_page: ÐœÐ¾Ñ Ñтраница + label_my_page_block: Блок моей Ñтраницы + label_my_projects: Мои проекты + label_new: Ðовый + label_new_statuses_allowed: Разрешенные новые ÑтатуÑÑ‹ + label_news_added: Добавлена новоÑть + label_news_latest: ПоÑледние новоÑти + label_news_new: Добавить новоÑть + label_news_plural: ÐовоÑти + label_news_view_all: ПоÑмотреть вÑе новоÑти + label_news: ÐовоÑти + label_next: Следующее + label_nobody: никто + label_no_change_option: (Ðет изменений) + label_no_data: Ðет данных Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ + label_none: отÑутÑтвует + label_not_contains: не Ñодержит + label_not_equals: не ÑоответÑтвует + label_open_issues: открыто + label_open_issues_plural: открыто + label_open_issues_plural2: открыто + label_open_issues_plural5: открыто + label_optional_description: ОпиÑание (необÑзательно) + label_options: Опции + label_overall_activity: Сводный отчёт дейÑтвий + label_overview: Обзор + label_password_lost: ВоÑÑтановление Ð¿Ð°Ñ€Ð¾Ð»Ñ + label_permissions_report: Отчёт по правам доÑтупа + label_permissions: Права доÑтупа + label_per_page: Ðа Ñтраницу + label_personalize_page: ПерÑонализировать данную Ñтраницу + label_planning: Планирование + label_please_login: ПожалуйÑта, войдите. + label_plugins: Модули + label_precedes: ÑÐ»ÐµÐ´ÑƒÑŽÑ‰Ð°Ñ + label_preferences: ÐŸÑ€ÐµÐ´Ð¿Ð¾Ñ‡Ñ‚ÐµÐ½Ð¸Ñ + label_preview: ПредпроÑмотр + label_previous: Предыдущее + label_profile: Профиль + label_project: Проект + label_project_all: Ð’Ñе проекты + label_project_copy_notifications: ОтправлÑть ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñлектронной почте при копировании проекта + label_project_latest: ПоÑледние проекты + label_project_new: Ðовый проект + label_project_plural: Проекты + label_project_plural2: проекта + label_project_plural5: проектов + label_public_projects: Общие проекты + label_query: Сохранённый Ð·Ð°Ð¿Ñ€Ð¾Ñ + label_query_new: Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ + label_query_plural: Сохранённые запроÑÑ‹ + label_read: Чтение... + label_register: РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ + label_registered_on: ЗарегиÑтрирован(а) + label_registration_activation_by_email: Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ñ‹Ñ… запиÑей по email + label_registration_automatic_activation: автоматичеÑÐºÐ°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ñ‹Ñ… запиÑей + label_registration_manual_activation: активировать учётные запиÑи вручную + label_related_issues: СвÑзанные задачи + label_relates_to: ÑвÑзана Ñ + label_relation_delete: Удалить ÑвÑзь + label_relation_new: Ðовое отношение + label_renamed: переименовано + label_reply_plural: Ответы + label_report: Отчёт + label_report_plural: Отчёты + label_reported_issues: Созданные задачи + label_repository: Хранилище + label_repository_plural: Хранилища + label_result_plural: Результаты + label_reverse_chronological_order: Ð’ обратном порÑдке + label_revision: Ð ÐµÐ´Ð°ÐºÑ†Ð¸Ñ + label_revision_plural: Редакции + label_roadmap: Оперативный план + label_roadmap_due_in: "Ð’ Ñрок %{value}" + label_roadmap_no_issues: Ðет задач Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ верÑии + label_roadmap_overdue: "опоздание %{value}" + label_role: Роль + label_role_and_permissions: Роли и права доÑтупа + label_role_new: ÐÐ¾Ð²Ð°Ñ Ñ€Ð¾Ð»ÑŒ + label_role_plural: Роли + label_scm: Тип хранилища + label_search: ПоиÑк + label_search_titles_only: ИÑкать только в названиÑÑ… + label_send_information: Отправить пользователю информацию по учётной запиÑи + label_send_test_email: ПоÑлать email Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸ + label_settings: ÐаÑтройки + label_show_completed_versions: Показывать завершённые верÑии + label_sort: Сортировать + label_sort_by: "Сортировать по %{value}" + label_sort_higher: Вверх + label_sort_highest: Ð’ начало + label_sort_lower: Вниз + label_sort_lowest: Ð’ конец + label_spent_time: Затраченное Ð²Ñ€ÐµÐ¼Ñ + label_start_to_end: Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° к концу + label_start_to_start: Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° к началу + label_statistics: СтатиÑтика + label_stay_logged_in: ОÑтаватьÑÑ Ð² ÑиÑтеме + label_string: ТекÑÑ‚ + label_subproject_plural: Подпроекты + label_subtask_plural: Подзадачи + label_text: Длинный текÑÑ‚ + label_theme: Тема + label_this_month: Ñтот меÑÑц + label_this_week: на Ñтой неделе + label_this_year: Ñтот год + label_time_tracking: Учёт времени + label_timelog_today: РаÑход времени на ÑÐµÐ³Ð¾Ð´Ð½Ñ + label_today: ÑÐµÐ³Ð¾Ð´Ð½Ñ + label_topic_plural: Темы + label_total: Ð’Ñего + label_tracker: Трекер + label_tracker_new: Ðовый трекер + label_tracker_plural: Трекеры + label_updated_time: "Обновлено %{value} назад" + label_updated_time_by: "Обновлено %{author} %{age} назад" + label_used_by: ИÑпользуетÑÑ + label_user: Пользователь + label_user_activity: "ДейÑÑ‚Ð²Ð¸Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{value}" + label_user_mail_no_self_notified: "Ðе извещать об изменениÑÑ…, которые Ñ Ñделал Ñам" + label_user_mail_option_all: "О вÑех ÑобытиÑÑ… во вÑех моих проектах" + label_user_mail_option_selected: "О вÑех ÑобытиÑÑ… только в выбранном проекте..." + label_user_mail_option_only_owner: Только Ð´Ð»Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð¾Ð², Ð´Ð»Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… Ñ ÑвлÑÑŽÑÑŒ владельцем + label_user_mail_option_only_my_events: Только Ð´Ð»Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð¾Ð², которые Ñ Ð¾Ñ‚Ñлеживаю или в которых учаÑтвую + label_user_mail_option_only_assigned: Только Ð´Ð»Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð¾Ð², которые назначены мне + label_user_new: Ðовый пользователь + label_user_plural: Пользователи + label_version: ВерÑÐ¸Ñ + label_version_new: ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ + label_version_plural: ВерÑии + label_view_diff: ПроÑмотреть Ð¾Ñ‚Ð»Ð¸Ñ‡Ð¸Ñ + label_view_revisions: ПроÑмотреть редакции + label_watched_issues: ОтÑлеживаемые задачи + label_week: ÐÐµÐ´ÐµÐ»Ñ + label_wiki: Wiki + label_wiki_edit: Редактирование Wiki + label_wiki_edit_plural: Wiki + label_wiki_page: Страница Wiki + label_wiki_page_plural: Страницы Wiki + label_workflow: ПоÑледовательноÑть дейÑтвий + label_x_closed_issues_abbr: + zero: "0 закрыто" + one: "%{count} закрыта" + few: "%{count} закрыто" + many: "%{count} закрыто" + other: "%{count} закрыто" + label_x_comments: + zero: "нет комментариев" + one: "%{count} комментарий" + few: "%{count} комментариÑ" + many: "%{count} комментариев" + other: "%{count} комментариев" + label_x_open_issues_abbr: + zero: "0 открыто" + one: "%{count} открыта" + few: "%{count} открыто" + many: "%{count} открыто" + other: "%{count} открыто" + label_x_open_issues_abbr_on_total: + zero: "0 открыто / %{total}" + one: "%{count} открыта / %{total}" + few: "%{count} открыто / %{total}" + many: "%{count} открыто / %{total}" + other: "%{count} открыто / %{total}" + label_x_projects: + zero: "нет проектов" + one: "%{count} проект" + few: "%{count} проекта" + many: "%{count} проектов" + other: "%{count} проектов" + label_year: Год + label_yesterday: вчера + + mail_body_account_activation_request: "ЗарегиÑтрирован новый пользователь (%{value}). Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ ожидает Вашего утверждениÑ:" + mail_body_account_information: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ Вашей учётной запиÑи + mail_body_account_information_external: "Ð’Ñ‹ можете иÑпользовать Вашу %{value} учётную запиÑÑŒ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ð°." + mail_body_lost_password: 'Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð¿Ñ€Ð¾Ð¹Ð´Ð¸Ñ‚Ðµ по Ñледующей ÑÑылке:' + mail_body_register: 'Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учётной запиÑи пройдите по Ñледующей ÑÑылке:' + mail_body_reminder: "%{count} назначенных на Ð’Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡ на Ñледующие %{days} дней:" + mail_subject_account_activation_request: "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° активацию Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð² ÑиÑтеме %{value}" + mail_subject_lost_password: "Ваш %{value} пароль" + mail_subject_register: "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ð¾Ð¹ запиÑи %{value}" + mail_subject_reminder: "%{count} назначенных на Ð’Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡ в ближайшие %{days} дней" + + notice_account_activated: Ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ активирована. Ð’Ñ‹ можете войти. + notice_account_invalid_creditentials: Ðеправильное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ пароль + notice_account_lost_email_sent: Вам отправлено пиÑьмо Ñ Ð¸Ð½ÑтрукциÑми по выбору нового паролÑ. + notice_account_password_updated: Пароль уÑпешно обновлён. + notice_account_pending: "Ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Ñоздана и ожидает Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратора." + notice_account_register_done: Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ уÑпешно Ñоздана. Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ Вашей учётной запиÑи пройдите по ÑÑылке, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð²Ñ‹Ñлана Вам по Ñлектронной почте. + notice_account_unknown_email: ÐеизвеÑтный пользователь. + notice_account_updated: Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ уÑпешно обновлена. + notice_account_wrong_password: Ðеверный пароль + notice_can_t_change_password: Ð”Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ учётной запиÑи иÑпользуетÑÑ Ð¸Ñточник внешней аутентификации. Ðевозможно изменить пароль. + notice_default_data_loaded: Была загружена ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¿Ð¾ умолчанию. + notice_email_error: "Во Ð²Ñ€ÐµÐ¼Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸ пиÑьма произошла ошибка (%{value})" + notice_email_sent: "Отправлено пиÑьмо %{value}" + notice_failed_to_save_issues: "Ðе удалоÑÑŒ Ñохранить %{count} пункт(ов) из %{total} выбранных: %{ids}." + notice_failed_to_save_members: "Ðе удалоÑÑŒ Ñохранить учаÑтника(ов): %{errors}." + notice_feeds_access_key_reseted: Ваш ключ доÑтупа Atom был Ñброшен. + notice_file_not_found: Страница, на которую Ð’Ñ‹ пытаетеÑÑŒ зайти, не ÑущеÑтвует или удалена. + notice_locking_conflict: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð° другим пользователем. + notice_no_issue_selected: "Ðе выбрано ни одной задачи! ПожалуйÑта, отметьте задачи, которые Ð’Ñ‹ хотите отредактировать." + notice_not_authorized: У Ð’Ð°Ñ Ð½ÐµÑ‚ прав Ð´Ð»Ñ Ð¿Ð¾ÑÐµÑ‰ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ Ñтраницы. + notice_successful_connection: Подключение уÑпешно уÑтановлено. + notice_successful_create: Создание уÑпешно. + notice_successful_delete: Удаление уÑпешно. + notice_successful_update: Обновление уÑпешно. + notice_unable_delete_version: Ðевозможно удалить верÑию. + + permission_add_issues: Добавление задач + permission_add_issue_notes: Добавление примечаний + permission_add_issue_watchers: Добавление наблюдателей + permission_add_messages: Отправка Ñообщений + permission_browse_repository: ПроÑмотр хранилища + permission_comment_news: Комментирование новоÑтей + permission_commit_access: Изменение файлов в хранилище + permission_delete_issues: Удаление задач + permission_delete_messages: Удаление Ñообщений + permission_delete_own_messages: Удаление ÑобÑтвенных Ñообщений + permission_delete_wiki_pages: Удаление wiki-Ñтраниц + permission_delete_wiki_pages_attachments: Удаление прикреплённых файлов + permission_edit_issue_notes: Редактирование примечаний + permission_edit_issues: Редактирование задач + permission_edit_messages: Редактирование Ñообщений + permission_edit_own_issue_notes: Редактирование ÑобÑтвенных примечаний + permission_edit_own_messages: Редактирование ÑобÑтвенных Ñообщений + permission_edit_own_time_entries: Редактирование ÑобÑтвенного учёта времени + permission_edit_project: Редактирование проектов + permission_edit_time_entries: Редактирование учёта времени + permission_edit_wiki_pages: Редактирование wiki-Ñтраниц + permission_export_wiki_pages: ЭкÑпорт wiki-Ñтраниц + permission_log_time: Учёт затраченного времени + permission_view_changesets: ПроÑмотр изменений хранилища + permission_view_time_entries: ПроÑмотр затраченного времени + permission_manage_project_activities: Управление типами дейÑтвий Ð´Ð»Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð° + permission_manage_boards: Управление форумами + permission_manage_categories: Управление категориÑми задач + permission_manage_files: Управление файлами + permission_manage_issue_relations: Управление ÑвÑзыванием задач + permission_manage_members: Управление учаÑтниками + permission_manage_news: Управление новоÑÑ‚Ñми + permission_manage_public_queries: Управление общими запроÑами + permission_manage_repository: Управление хранилищем + permission_manage_subtasks: Управление подзадачами + permission_manage_versions: Управление верÑиÑми + permission_manage_wiki: Управление Wiki + permission_move_issues: ÐŸÐµÑ€ÐµÐ½Ð¾Ñ Ð·Ð°Ð´Ð°Ñ‡ + permission_protect_wiki_pages: Блокирование wiki-Ñтраниц + permission_rename_wiki_pages: Переименование wiki-Ñтраниц + permission_save_queries: Сохранение запроÑов + permission_select_project_modules: Выбор модулей проекта + permission_view_calendar: ПроÑмотр ÐºÐ°Ð»ÐµÐ½Ð´Ð°Ñ€Ñ + permission_view_documents: ПроÑмотр документов + permission_view_files: ПроÑмотр файлов + permission_view_gantt: ПроÑмотр диаграммы Ганта + permission_view_issue_watchers: ПроÑмотр ÑпиÑка наблюдателей + permission_view_messages: ПроÑмотр Ñообщений + permission_view_wiki_edits: ПроÑмотр иÑтории Wiki + permission_view_wiki_pages: ПроÑмотр Wiki + + project_module_boards: Форумы + project_module_documents: Документы + project_module_files: Файлы + project_module_issue_tracking: Задачи + project_module_news: ÐовоÑти + project_module_repository: Хранилище + project_module_time_tracking: Учёт времени + project_module_wiki: Wiki + project_module_gantt: Диаграмма Ганта + project_module_calendar: Календарь + + setting_activity_days_default: КоличеÑтво дней, отображаемых в ДейÑтвиÑÑ… + setting_app_subtitle: Подзаголовок Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + setting_app_title: Ðазвание Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + setting_attachment_max_size: МакÑимальный размер Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + setting_autofetch_changesets: ÐвтоматичеÑки Ñледить за изменениÑми хранилища + setting_autologin: ÐвтоматичеÑкий вход + setting_bcc_recipients: ИÑпользовать Ñкрытые копии (BCC) + setting_cache_formatted_text: Кешировать форматированный текÑÑ‚ + setting_commit_fix_keywords: Ðазначение ключевых Ñлов + setting_commit_ref_keywords: Ключевые Ñлова Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñка + setting_cross_project_issue_relations: Разрешить переÑечение задач по проектам + setting_date_format: Формат даты + setting_default_language: Язык по умолчанию + setting_default_notification_option: СпоÑоб Ð¾Ð¿Ð¾Ð²ÐµÑ‰ÐµÐ½Ð¸Ñ Ð¿Ð¾ умолчанию + setting_default_projects_public: Ðовые проекты ÑвлÑÑŽÑ‚ÑÑ Ð¾Ð±Ñ‰ÐµÐ´Ð¾Ñтупными + setting_diff_max_lines_displayed: МакÑимальное чиÑло Ñтрок Ð´Ð»Ñ diff + setting_display_subprojects_issues: Отображение подпроектов по умолчанию + setting_emails_footer: ПодÑтрочные Ð¿Ñ€Ð¸Ð¼ÐµÑ‡Ð°Ð½Ð¸Ñ Ð¿Ð¸Ñьма + setting_enabled_scm: Включённые SCM + setting_feeds_limit: Ограничение количеÑтва заголовков Ð´Ð»Ñ Atom потока + setting_file_max_size_displayed: МакÑимальный размер текÑтового файла Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ + setting_gravatar_enabled: ИÑпользовать аватар Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð· Gravatar + setting_host_name: Ð˜Ð¼Ñ ÐºÐ¾Ð¼Ð¿ÑŒÑŽÑ‚ÐµÑ€Ð° + setting_issue_list_default_columns: Столбцы, отображаемые в ÑпиÑке задач по умолчанию + setting_issues_export_limit: Ограничение по ÑкÑпортируемым задачам + setting_login_required: Ðеобходима Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ + setting_mail_from: ИÑходÑщий email Ð°Ð´Ñ€ÐµÑ + setting_mail_handler_api_enabled: Включить веб-ÑÐµÑ€Ð²Ð¸Ñ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñщих Ñообщений + setting_mail_handler_api_key: API ключ + setting_openid: Разрешить OpenID Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ð° и региÑтрации + setting_per_page_options: КоличеÑтво запиÑей на Ñтраницу + setting_plain_text_mail: Только проÑтой текÑÑ‚ (без HTML) + setting_protocol: Протокол + setting_repository_log_display_limit: МакÑимальное количеÑтво редакций, отображаемых в журнале изменений + setting_self_registration: СаморегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ + setting_sequential_project_identifiers: Генерировать поÑледовательные идентификаторы проектов + setting_sys_api_enabled: Включить веб-ÑÐµÑ€Ð²Ð¸Ñ Ð´Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð»Ð¸Ñ‰ÐµÐ¼ + setting_text_formatting: Форматирование текÑта + setting_time_format: Формат времени + setting_user_format: Формат Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ + setting_welcome_text: ТекÑÑ‚ приветÑÑ‚Ð²Ð¸Ñ + setting_wiki_compression: Сжатие иÑтории Wiki + + status_active: активен + status_locked: заблокирован + status_registered: зарегиÑтрирован + + text_are_you_sure: Ð’Ñ‹ уверены? + text_assign_time_entries_to_project: Прикрепить зарегиÑтрированное Ð²Ñ€ÐµÐ¼Ñ Ðº проекту + text_caracters_maximum: "МакÑимум %{count} Ñимволов(а)." + text_caracters_minimum: "Должно быть не менее %{count} Ñимволов." + text_comma_separated: ДопуÑтимы неÑколько значений (через запÑтую). + text_custom_field_possible_values_info: 'По одному значению в каждой Ñтроке' + text_default_administrator_account_changed: Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ админиÑтратора по умолчанию изменена + text_destroy_time_entries_question: "Ðа Ñту задачу зарегиÑтрировано %{hours} чаÑа(ов) затраченного времени. Что Ð’Ñ‹ хотите предпринÑть?" + text_destroy_time_entries: Удалить зарегиÑтрированное Ð²Ñ€ÐµÐ¼Ñ + text_diff_truncated: '... Этот diff ограничен, так как превышает макÑимальный отображаемый размер.' + text_email_delivery_not_configured: "Параметры работы Ñ Ð¿Ð¾Ñ‡Ñ‚Ð¾Ð²Ñ‹Ð¼ Ñервером не наÑтроены и Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ email не активна.\nÐаÑтроить параметры Ð´Ð»Ñ Ð’Ð°ÑˆÐµÐ³Ð¾ SMTP-Ñервера Ð’Ñ‹ можете в файле config/configuration.yml. Ð”Ð»Ñ Ð¿Ñ€Ð¸Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹ перезапуÑтите приложение." + text_enumeration_category_reassign_to: 'Ðазначить им Ñледующее значение:' + text_enumeration_destroy_question: "%{count} объект(а,ов) ÑвÑзаны Ñ Ñтим значением." + text_file_repository_writable: Хранилище файлов доÑтупно Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñи + text_issue_added: "Создана Ð½Ð¾Ð²Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° %{id} (%{author})." + text_issue_category_destroy_assignments: Удалить Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ð¸ + text_issue_category_destroy_question: "ÐеÑколько задач (%{count}) назначено в данную категорию. Что Ð’Ñ‹ хотите предпринÑть?" + text_issue_category_reassign_to: Переназначить задачи Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ категории + text_issues_destroy_confirmation: 'Ð’Ñ‹ уверены, что хотите удалить выбранные задачи?' + text_issues_ref_in_commit_messages: СопоÑтавление и изменение ÑтатуÑа задач иÑÑ…Ð¾Ð´Ñ Ð¸Ð· текÑта Ñообщений + text_issue_updated: "Задача %{id} была обновлена (%{author})." + text_journal_changed: "Параметр %{label} изменилÑÑ Ñ %{old} на %{new}" + text_journal_deleted: "Значение %{old} параметра %{label} удалено" + text_journal_set_to: "Параметр %{label} изменилÑÑ Ð½Ð° %{value}" + text_length_between: "Длина между %{min} и %{max} Ñимволов." + text_load_default_configuration: Загрузить конфигурацию по умолчанию + text_min_max_length_info: 0 означает отÑутÑтвие ограничений + text_no_configuration_data: "Роли, трекеры, ÑтатуÑÑ‹ задач и оперативный план не были Ñконфигурированы.\nÐаÑтоÑтельно рекомендуетÑÑ Ð·Ð°Ð³Ñ€ÑƒÐ·Ð¸Ñ‚ÑŒ конфигурацию по-умолчанию. Ð’Ñ‹ Ñможете её изменить потом." + text_plugin_assets_writable: Каталог реÑурÑов модулей доÑтупен Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñи + text_project_destroy_confirmation: Ð’Ñ‹ наÑтаиваете на удалении данного проекта и вÑей отноÑÑщейÑÑ Ðº нему информации? + text_reassign_time_entries: 'ПеренеÑти зарегиÑтрированное Ð²Ñ€ÐµÐ¼Ñ Ð½Ð° Ñледующую задачу:' + text_regexp_info: "например: ^[A-Z0-9]+$" + text_repository_usernames_mapping: "Выберите или обновите Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Redmine, ÑвÑзанного Ñ Ð½Ð°Ð¹Ð´ÐµÐ½Ð½Ñ‹Ð¼Ð¸ именами в журнале хранилища.\nПользователи Ñ Ð¾Ð´Ð¸Ð½Ð°ÐºÐ¾Ð²Ñ‹Ð¼Ð¸ именами или email в Redmine и хранилище ÑвÑзываютÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑки." + text_rmagick_available: ДоÑтупно иÑпользование RMagick (опционально) + text_select_mail_notifications: Выберите дейÑтвиÑ, при которых будет отÑылатьÑÑ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ðµ на Ñлектронную почту. + text_select_project_modules: 'Выберите модули, которые будут иÑпользованы в проекте:' + text_status_changed_by_changeset: "Реализовано в %{value} редакции." + text_subprojects_destroy_warning: "Подпроекты: %{value} также будут удалены." + text_tip_issue_begin_day: дата начала задачи + text_tip_issue_begin_end_day: начало задачи и окончание её в Ñтот же день + text_tip_issue_end_day: дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ + text_tracker_no_workflow: Ð”Ð»Ñ Ñтого трекера поÑледовательноÑть дейÑтвий не определена + text_unallowed_characters: Запрещенные Ñимволы + text_user_mail_option: "Ð”Ð»Ñ Ð½ÐµÐ²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ñ‹Ñ… проектов, Ð’Ñ‹ будете получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ о том, что проÑматриваете или в чем учаÑтвуете (например, задачи, автором которых Ð’Ñ‹ ÑвлÑетеÑÑŒ, или которые Вам назначены)." + text_user_wrote: "%{value} пиÑал(а):" + text_wiki_destroy_confirmation: Ð’Ñ‹ уверены, что хотите удалить данную Wiki и вÑе её Ñодержимое? + text_workflow_edit: Выберите роль и трекер Ð´Ð»Ñ Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÑледовательноÑти ÑоÑтоÑний + + warning_attachments_not_saved: "%{count} файл(ов) невозможно Ñохранить." + text_wiki_page_destroy_question: Эта Ñтраница имеет %{descendants} дочерних Ñтраниц и их потомков. Что вы хотите предпринÑть? + text_wiki_page_reassign_children: Переопределить дочерние Ñтраницы на текущую Ñтраницу + text_wiki_page_nullify_children: Сделать дочерние Ñтраницы главными Ñтраницами + text_wiki_page_destroy_children: Удалить дочерние Ñтраницы и вÑех их потомков + setting_password_min_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° Ð¿Ð°Ñ€Ð¾Ð»Ñ + field_group_by: Группировать результаты по + mail_subject_wiki_content_updated: "Wiki-Ñтраница '%{id}' была обновлена" + label_wiki_content_added: Добавлена wiki-Ñтраница + mail_subject_wiki_content_added: "Wiki-Ñтраница '%{id}' была добавлена" + mail_body_wiki_content_added: "%{author} добавил(а) wiki-Ñтраницу '%{id}'." + label_wiki_content_updated: Обновлена wiki-Ñтраница + mail_body_wiki_content_updated: "%{author} обновил(а) wiki-Ñтраницу '%{id}'." + permission_add_project: Создание проекта + setting_new_project_user_role_id: Роль, Ð½Ð°Ð·Ð½Ð°Ñ‡Ð°ÐµÐ¼Ð°Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÑŽ, Ñоздавшему проект + label_view_all_revisions: Показать вÑе ревизии + label_tag: Метка + label_branch: Ветвь + error_no_tracker_in_project: С Ñтим проектом не аÑÑоциирован ни один трекер. Проверьте наÑтройки проекта. + error_no_default_issue_status: Ðе определен ÑÑ‚Ð°Ñ‚ÑƒÑ Ð·Ð°Ð´Ð°Ñ‡ по умолчанию. Проверьте наÑтройки (Ñм. "ÐдминиÑтрирование -> СтатуÑÑ‹ задач"). + label_group_plural: Группы + label_group: Группа + label_group_new: ÐÐ¾Ð²Ð°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð° + label_time_entry_plural: Затраченное Ð²Ñ€ÐµÐ¼Ñ + text_journal_added: "%{label} %{value} добавлен" + field_active: Ðктивно + enumeration_system_activity: СиÑтемное + permission_delete_issue_watchers: Удаление наблюдателей + version_status_closed: закрыт + version_status_locked: заблокирован + version_status_open: открыт + error_can_not_reopen_issue_on_closed_version: Задача, Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ð°Ñ Ðº закрытой верÑии, не Ñможет быть открыта Ñнова + label_user_anonymous: Ðноним + button_move_and_follow: ПеремеÑтить и перейти + setting_default_projects_modules: Включенные по умолчанию модули Ð´Ð»Ñ Ð½Ð¾Ð²Ñ‹Ñ… проектов + setting_gravatar_default: Изображение Gravatar по умолчанию + field_sharing: СовмеÑтное иÑпользование + label_version_sharing_hierarchy: С иерархией проектов + label_version_sharing_system: Со вÑеми проектами + label_version_sharing_descendants: С подпроектами + label_version_sharing_tree: С деревом проектов + label_version_sharing_none: Без ÑовмеÑтного иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ + error_can_not_archive_project: Этот проект не может быть заархивирован + button_duplicate: Дублировать + button_copy_and_follow: Копировать и продолжить + label_copy_source: ИÑточник + setting_issue_done_ratio: РаÑÑчитывать готовноÑть задачи Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ð¿Ð¾Ð»Ñ + setting_issue_done_ratio_issue_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð·Ð°Ð´Ð°Ñ‡Ð¸ + error_issue_done_ratios_not_updated: Параметр готовноÑть задач не обновлён + error_workflow_copy_target: Выберите целевые трекеры и роли + setting_issue_done_ratio_issue_field: ГотовноÑть задачи + label_copy_same_as_target: То же, что и у цели + label_copy_target: Цель + notice_issue_done_ratios_updated: Параметр «Ð³Ð¾Ñ‚овноÑть» обновлён. + error_workflow_copy_source: Выберите иÑходный трекер или роль + label_update_issue_done_ratios: Обновить готовноÑть задач + setting_start_of_week: День начала недели + label_api_access_key: Ключ доÑтупа к API + text_line_separated: Разрешено неÑколько значений (по одному значению в Ñтроку). + label_revision_id: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ %{value} + permission_view_issues: ПроÑмотр задач + label_display_used_statuses_only: Отображать только те ÑтатуÑÑ‹, которые иÑпользуютÑÑ Ð² Ñтом трекере + label_api_access_key_created_on: Ключ доÑтуп к API был Ñоздан %{value} назад + label_feeds_access_key: Ключ доÑтупа к Atom + notice_api_access_key_reseted: Ваш ключ доÑтупа к API был Ñброшен. + setting_rest_api_enabled: Включить веб-ÑÐµÑ€Ð²Ð¸Ñ REST + button_show: Показать + label_missing_api_access_key: ОтÑутÑтвует ключ доÑтупа к API + label_missing_feeds_access_key: ОтÑутÑтвует ключ доÑтупа к Atom + setting_mail_handler_body_delimiters: Урезать пиÑьмо поÑле одной из Ñтих Ñтрок + permission_add_subprojects: Создание подпроектов + label_subproject_new: Ðовый подпроект + text_own_membership_delete_confirmation: |- + Ð’Ñ‹ ÑобираетеÑÑŒ удалить некоторые или вÑе права, из-за чего могут пропаÑть права на редактирование Ñтого проекта. + Продолжить? + label_close_versions: Закрыть завершённые верÑии + label_board_sticky: Прикреплена + label_board_locked: Заблокирована + field_principal: Ð˜Ð¼Ñ + text_zoom_out: Отдалить + text_zoom_in: Приблизить + notice_unable_delete_time_entry: Ðевозможно удалить запиÑÑŒ журнала. + label_overall_spent_time: Ð’Ñего затрачено времени + label_user_mail_option_none: Ðет Ñобытий + field_member_of_group: Группа назначенного + field_assigned_to_role: Роль назначенного + notice_not_authorized_archived_project: Запрашиваемый проект был архивирован. + label_principal_search: "Ðайти Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ группу:" + label_user_search: "Ðайти пользователÑ:" + field_visible: Видимое + setting_emails_header: Заголовок пиÑьма + + setting_commit_logtime_activity_id: ДейÑтвие Ð´Ð»Ñ ÑƒÑ‡Ñ‘Ñ‚Ð° времени + text_time_logged_by_changeset: Учтено в редакции %{value}. + setting_commit_logtime_enabled: Включить учёт времени + notice_gantt_chart_truncated: Диаграмма будет уÑечена, поÑкольку превышено макÑимальное кол-во Ñлементов, которые могут отображатьÑÑ (%{max}) + setting_gantt_items_limit: МакÑимальное кол-во Ñлементов отображаемых на диаграмме Ганта + 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: Ðноним + label_role_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: Модуль + field_cvsroot: CVSROOT + text_mercurial_repository_note: Локальное хранилище (например, /hgrepo, c:\hgrepo) + text_scm_command: Команда + text_scm_command_version: ВерÑÐ¸Ñ + label_git_report_last_commit: Указывать поÑледнее Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ„Ð°Ð¹Ð»Ð¾Ð² и директорий + text_scm_config: Ð’Ñ‹ можете наÑтроить команды SCM в файле config/configuration.yml. ПожалуйÑта, перезапуÑтите приложение поÑле Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñтого файла. + text_scm_command_not_available: Команда ÑиÑтемы ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»Ñ Ð²ÐµÑ€Ñий недоÑтупна. ПожалуйÑта, проверьте наÑтройки в админиÑтративной панели. + notice_issue_successful_create: Задача %{id} Ñоздана. + label_between: между + setting_issue_group_assignment: Разрешить назначение задач группам пользователей + label_diff: Разница(diff) + text_git_repository_note: Хранилище пуÑтое и локальное (Ñ‚.е. /gitrepo, c:\gitrepo) + 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: "Ðевозможно Ñохранить %{count} затраченное Ð²Ñ€ÐµÐ¼Ñ Ð´Ð»Ñ %{total} выбранных: %{ids}." + label_x_issues: + one: "%{count} задача" + few: "%{count} задачи" + many: "%{count} задач" + 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), цифры, тире и подчеркиваниÑ.
    ПоÑле ÑÐ¾Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¸Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ‚Ð¾Ñ€ изменить нельзÑ. + field_multiple: МножеÑтвенные Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ + setting_commit_cross_project_ref: Разрешить ÑÑылатьÑÑ Ð¸ иÑправлÑть задачи во вÑех оÑтальных проектах + text_issue_conflict_resolution_add_notes: Добавить мои Ð¿Ñ€Ð¸Ð¼ÐµÑ‡Ð°Ð½Ð¸Ñ Ð¸ отказатьÑÑ Ð¾Ñ‚ моих изменений + text_issue_conflict_resolution_overwrite: Применить мои Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ (вÑе предыдущие Ð·Ð°Ð¼ÐµÑ‡Ð°Ð½Ð¸Ñ Ð±ÑƒÐ´ÑƒÑ‚ Ñохранены, но некоторые Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¼Ð¾Ð³ÑƒÑ‚ быть перезапиÑаны) + notice_issue_update_conflict: Кто-то изменил задачу, пока вы ее редактировали. + text_issue_conflict_resolution_cancel: Отменить мои Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸ показать задачу заново %{link} + permission_manage_related_issues: Управление ÑвÑзанными задачами + field_auth_source_ldap_filter: Фильтр LDAP + label_search_for_watchers: Ðайти наблюдателей + notice_account_deleted: "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ полноÑтью удалена" + setting_unsubscribe: "Разрешить пользователÑм удалÑть Ñвои учетные запиÑи" + button_delete_my_account: "Удалить мою учетную запиÑÑŒ" + text_account_destroy_confirmation: "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ будет полноÑтью удалена без возможноÑти воÑÑтановлениÑ.\nÐ’Ñ‹ уверены, что хотите продолжить?" + error_session_expired: Срок вашей ÑеÑÑии иÑтек. ПожалуйÑта войдите еще раз + text_session_expiration_settings: "Внимание! Изменение Ñтих наÑтроек может привеÑти к завершению текущих ÑеÑÑий, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ð²Ð°ÑˆÑƒ." + setting_session_lifetime: МакÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð»Ð¶Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñть ÑеÑÑии + setting_session_timeout: Таймаут ÑеÑÑии + label_session_expiration: Срок иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ ÑеÑÑии + permission_close_project: Закрывать / открывать проекты + label_show_closed_projects: ПроÑматривать закрытые проекты + button_close: Сделать закрытым + button_reopen: Сделать открытым + project_status_active: открытые + project_status_closed: закрытые + project_status_archived: архивированные + text_project_closed: Проект закрыт и находитÑÑ Ð² режиме только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ. + notice_user_successful_create: Пользователь %{id} Ñоздан. + field_core_fields: Стандартные Ð¿Ð¾Ð»Ñ + field_timeout: Таймаут (в Ñекундах) + setting_thumbnails_enabled: Отображать превью Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ + setting_thumbnails_size: Размер первью (в пикÑелÑÑ…) + label_status_transitions: СтатуÑ-переходы + label_fields_permissions: Права на Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÐµÐ¹ + label_readonly: Ðе изменÑетÑÑ + label_required: ОбÑзательное + text_repository_identifier_info: ДопуÑкаютÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ñтрочные латинÑкие буквы (a-z), цифры, тире и подчеркиваниÑ.
    ПоÑле ÑÐ¾Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¸Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ‚Ð¾Ñ€ изменить нельзÑ. + field_board_parent: РодительÑкий форум + label_attribute_of_project: Проект %{name} + label_attribute_of_author: Ð˜Ð¼Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð° %{name} + label_attribute_of_assigned_to: Ðазначена %{name} + label_attribute_of_fixed_version: ВерÑÐ¸Ñ %{name} + label_copy_subtasks: Копировать подзадачи + label_copied_to: Ñкопирована в + label_copied_from: Ñкопирована Ñ + label_any_issues_in_project: любые задачи в проекте + label_any_issues_not_in_project: любые задачи не в проекте + field_private_notes: Приватный комментарий + permission_view_private_notes: ПроÑмотр приватных комментариев + permission_set_notes_private: Размещение приватных комментариев + label_no_issues_in_project: нет задач в проекте + label_any: вÑе + label_last_n_weeks: + one: "поÑледнÑÑ %{count} неделÑ" + few: "поÑледние %{count} недели" + many: "поÑледние %{count} недель" + other: "поÑледние %{count} недели" + setting_cross_project_subtasks: Разрешить подзадачи в между проектами + label_cross_project_descendants: С подпроектами + label_cross_project_tree: С деревом проектов + label_cross_project_hierarchy: С иерархией проектов + label_cross_project_system: Со вÑеми проектами + button_hide: Скрыть + setting_non_working_week_days: Ðерабочие дни + label_in_the_next_days: в Ñредующие дни + label_in_the_past_days: в прошлые дни + label_attribute_of_user: Пользователь %{name} + text_turning_multiple_off: ЕÑли отключить множеÑтвенные значениÑ, лишние Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¸Ð· ÑпиÑка будут удалены, чтобы оÑталоÑÑŒ только по одному значению. + label_attribute_of_issue: Задача %{name} + permission_add_documents: Добавить документы + permission_edit_documents: Редактировать документы + permission_delete_documents: Удалить документы + label_gantt_progress_line: Ð›Ð¸Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð³Ñ€ÐµÑÑа + setting_jsonp_enabled: Поддержка JSONP + field_inherit_members: ÐаÑледовать учаÑтников + field_closed_on: Закрыта + field_generate_password: Создание Ð¿Ð°Ñ€Ð¾Ð»Ñ + setting_default_projects_tracker_ids: Трекеры по умолчанию Ð´Ð»Ñ Ð½Ð¾Ð²Ñ‹Ñ… проектов + label_total_time: Общее Ð²Ñ€ÐµÐ¼Ñ + notice_account_not_activated_yet: Ð’Ñ‹ пока не имеете активированных учетных запиÑей. + Чтобы получить пиÑьмо Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸ÐµÐ¹, перейдите по ÑÑылке. + notice_account_locked: Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ заблокирована. + label_hidden: Скрытый + label_visibility_private: только мне + label_visibility_roles: только Ñтим ролÑм + label_visibility_public: вÑем пользователÑм + field_must_change_passwd: Изменить пароль при Ñледующем входе + notice_new_password_must_be_different: Ðовый пароль должен отличатьÑÑ Ð¾Ñ‚ текущего + setting_mail_handler_excluded_filenames: ИÑключать Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾ имени + text_convert_available: ДоÑтупно иÑпользование ImageMagick (необÑзательно) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1d/1dd21beebda1cb6cbb50dd1ef6a53e3977411fdf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1dd21beebda1cb6cbb50dd1ef6a53e3977411fdf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,9 @@ +class AddCustomFieldsDescription < ActiveRecord::Migration + def up + add_column :custom_fields, :description, :text + end + + def down + remove_column :custom_fields, :description + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1e/1e34af29d75d4695760428516010f4b5a35d1aad.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1e34af29d75d4695760428516010f4b5a35d1aad.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,85 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 compute the start and end dates of a calendar + class Calendar + include Redmine::I18n + attr_reader :startdt, :enddt + + def initialize(date, lang = current_language, period = :month) + @date = date + @events = [] + @ending_events_by_days = {} + @starting_events_by_days = {} + set_language_if_valid lang + case period + when :month + @startdt = Date.civil(date.year, date.month, 1) + @enddt = (@startdt >> 1)-1 + # starts from the first day of the week + @startdt = @startdt - (@startdt.cwday - first_wday)%7 + # ends on the last day of the week + @enddt = @enddt + (last_wday - @enddt.cwday)%7 + when :week + @startdt = date - (date.cwday - first_wday)%7 + @enddt = date + (last_wday - date.cwday)%7 + else + raise 'Invalid period' + end + end + + # Sets calendar events + def events=(events) + @events = events + @ending_events_by_days = @events.group_by {|event| event.due_date} + @starting_events_by_days = @events.group_by {|event| event.start_date} + end + + # Returns events for the given day + def events_on(day) + ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq + end + + # Calendar current month + def month + @date.month + end + + # Return the first day of week + # 1 = Monday ... 7 = Sunday + def first_wday + case Setting.start_of_week.to_i + when 1 + @first_dow ||= (1 - 1)%7 + 1 + when 6 + @first_dow ||= (6 - 1)%7 + 1 + when 7 + @first_dow ||= (7 - 1)%7 + 1 + else + @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1 + end + end + + def last_wday + @last_dow ||= (first_wday + 5)%7 + 1 + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1e/1e36d3b0b9a7b1dcdb385f344cf25506fbe7bce9.svn-base --- a/.svn/pristine/1e/1e36d3b0b9a7b1dcdb385f344cf25506fbe7bce9.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -

    <%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)

    - -<%= form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %> - <%= render :partial => auth_source_partial_name(@auth_source) %> - <%= submit_tag l(:button_save) %> -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1e/1e8f4706c992f648980e1bbef5b267351fd0e7ee.svn-base --- a/.svn/pristine/1e/1e8f4706c992f648980e1bbef5b267351fd0e7ee.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ - - - - - - <% if tab[:name] == 'IssueCustomField' %> - - - <% end %> - - - - - <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> - "> - - - - <% if tab[:name] == 'IssueCustomField' %> - - - <% end %> - - - - <% end; reset_cycle %> - -
    <%=l(:field_name)%><%=l(:field_field_format)%><%=l(:field_is_required)%><%=l(:field_is_for_all)%><%=l(:label_used_by)%><%=l(:button_sort)%>
    <%= link_to h(custom_field.name), edit_custom_field_path(custom_field) %><%= l(Redmine::CustomFieldFormat.label_for(custom_field.field_format)) %><%= checked_image custom_field.is_required? %><%= checked_image custom_field.is_for_all? %><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %><%= reorder_links('custom_field', {:action => 'update', :id => custom_field}, :put) %> - <%= delete_link custom_field_path(custom_field) %> -
    - -

    <%= link_to l(:label_custom_field_new), new_custom_field_path(:type => tab[:name]), :class => 'icon icon-add' %>

    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1e/1ea62f0251b7b445e6492159a303f580aeb22b77.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1ea62f0251b7b445e6492159a303f580aeb22b77.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,291 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'redmine/scm/adapters/abstract_adapter' +require 'uri' + +module Redmine + module Scm + module Adapters + class SubversionAdapter < AbstractAdapter + + # SVN executable name + SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn" + + class << self + def client_command + @@bin ||= SVN_BIN + end + + def sq_bin + @@sq_bin ||= shell_quote_command + end + + def client_version + @@client_version ||= (svn_binary_version || []) + end + + def client_available + # --xml options are introduced in 1.3. + # http://subversion.apache.org/docs/release-notes/1.3.html + client_version_above?([1, 3]) + end + + def svn_binary_version + scm_version = scm_version_from_command_line.dup + if scm_version.respond_to?(:force_encoding) + scm_version.force_encoding('ASCII-8BIT') + end + if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}) + m[2].scan(%r{\d+}).collect(&:to_i) + end + end + + def scm_version_from_command_line + shellout("#{sq_bin} --version") { |io| io.read }.to_s + end + end + + # Get info about the svn repository + def info + cmd = "#{self.class.sq_bin} info --xml #{target}" + cmd << credentials_string + info = nil + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + # root_url = doc.elements["info/entry/repository/root"].text + info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'], + :lastrev => Revision.new({ + :identifier => doc['info']['entry']['commit']['revision'], + :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime, + :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "") + }) + }) + rescue + end + end + return nil if $? && $?.exitstatus != 0 + info + rescue CommandFailed + return nil + end + + # Returns an Entries collection + # or nil if the given path doesn't exist in the repository + def entries(path=nil, identifier=nil, options={}) + path ||= '' + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + entries = Entries.new + cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}" + cmd << credentials_string + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + each_xml_element(doc['lists']['list'], 'entry') do |entry| + commit = entry['commit'] + commit_date = commit['date'] + # Skip directory if there is no commit date (usually that + # means that we don't have read access to it) + next if entry['kind'] == 'dir' && commit_date.nil? + name = entry['name']['__content__'] + entries << Entry.new({:name => URI.unescape(name), + :path => ((path.empty? ? "" : "#{path}/") + name), + :kind => entry['kind'], + :size => ((s = entry['size']) ? s['__content__'].to_i : nil), + :lastrev => Revision.new({ + :identifier => commit['revision'], + :time => Time.parse(commit_date['__content__'].to_s).localtime, + :author => ((a = commit['author']) ? a['__content__'] : nil) + }) + }) + end + rescue Exception => e + logger.error("Error parsing svn output: #{e.message}") + logger.error("Output was:\n #{output}") + end + end + return nil if $? && $?.exitstatus != 0 + logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? + entries.sort_by_name + end + + def properties(path, identifier=nil) + # proplist xml output supported in svn 1.5.0 and higher + return nil unless self.class.client_version_above?([1, 5, 0]) + + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}" + cmd << credentials_string + properties = {} + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + each_xml_element(doc['properties']['target'], 'property') do |property| + properties[ property['name'] ] = property['__content__'].to_s + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + properties + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path ||= '' + identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" + identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1 + revisions = Revisions.new + cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}" + cmd << credentials_string + cmd << " --verbose " if options[:with_paths] + cmd << " --limit #{options[:limit].to_i}" if options[:limit] + cmd << ' ' + target(path) + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + each_xml_element(doc['log'], 'logentry') do |logentry| + paths = [] + each_xml_element(logentry['paths'], 'path') do |path| + paths << {:action => path['action'], + :path => path['__content__'], + :from_path => path['copyfrom-path'], + :from_revision => path['copyfrom-rev'] + } + end if logentry['paths'] && logentry['paths']['path'] + paths.sort! { |x,y| x[:path] <=> y[:path] } + + revisions << Revision.new({:identifier => logentry['revision'], + :author => (logentry['author'] ? logentry['author']['__content__'] : ""), + :time => Time.parse(logentry['date']['__content__'].to_s).localtime, + :message => logentry['msg']['__content__'], + :paths => paths + }) + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + revisions + end + + def diff(path, identifier_from, identifier_to=nil) + path ||= '' + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : '' + + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) + + cmd = "#{self.class.sq_bin} diff -r " + cmd << "#{identifier_to}:" + cmd << "#{identifier_from}" + cmd << " #{target(path)}@#{identifier_from}" + cmd << credentials_string + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + return nil if $? && $?.exitstatus != 0 + diff + end + + def cat(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}" + cmd << credentials_string + cat = nil + shellout(cmd) do |io| + io.binmode + cat = io.read + end + return nil if $? && $?.exitstatus != 0 + cat + end + + def annotate(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}" + cmd << credentials_string + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} + rev = $1 + blame.add_line($3.rstrip, + Revision.new( + :identifier => rev, + :revision => rev, + :author => $2.strip + )) + end + end + return nil if $? && $?.exitstatus != 0 + blame + end + + private + + def credentials_string + str = '' + str << " --username #{shell_quote(@login)}" unless @login.blank? + str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank? + str << " --no-auth-cache --non-interactive" + str + end + + # Helper that iterates over the child elements of a xml node + # MiniXml returns a hash when a single child is found + # or an array of hashes for multiple children + def each_xml_element(node, name) + if node && node[name] + if node[name].is_a?(Hash) + yield node[name] + else + node[name].each do |element| + yield element + end + end + end + end + + def target(path = '') + base = path.match(/^\//) ? root_url : url + uri = "#{base}/#{path}" + uri = URI.escape(URI.escape(uri), '[]') + shell_quote(uri.gsub(/[?<>\*]/, '')) + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1e/1ec171c5d371f7815766568fcf1c2d5feb1a3958.svn-base --- a/.svn/pristine/1e/1ec171c5d371f7815766568fcf1c2d5feb1a3958.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -<%= "#{issue.tracker.name} ##{issue.id}: #{issue.subject}" %> -<%= issue_url %> - -* <%=l(:field_author)%>: <%= issue.author %> -* <%=l(:field_status)%>: <%= issue.status %> -* <%=l(:field_priority)%>: <%= issue.priority %> -* <%=l(:field_assigned_to)%>: <%= issue.assigned_to %> -* <%=l(:field_category)%>: <%= issue.category %> -* <%=l(:field_fixed_version)%>: <%= issue.fixed_version %> -<% issue.custom_field_values.each do |c| %>* <%= c.custom_field.name %>: <%= show_value(c) %> -<% end -%> ----------------------------------------- -<%= issue.description %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1f2100d78a901e27c2b48a6bdb60bbd56653d6f4.svn-base --- a/.svn/pristine/1f/1f2100d78a901e27c2b48a6bdb60bbd56653d6f4.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 AuthSourcesController < ApplicationController - layout 'admin' - menu_item :ldap_authentication - - before_filter :require_admin - - def index - @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 10 - end - - def new - klass_name = params[:type] || 'AuthSourceLdap' - @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source]) - end - - def create - @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source]) - if @auth_source.save - flash[:notice] = l(:notice_successful_create) - redirect_to :action => 'index' - else - render :action => 'new' - end - end - - def edit - @auth_source = AuthSource.find(params[:id]) - end - - def update - @auth_source = AuthSource.find(params[:id]) - if @auth_source.update_attributes(params[:auth_source]) - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'index' - else - render :action => 'edit' - end - end - - def test_connection - @auth_source = AuthSource.find(params[:id]) - begin - @auth_source.test_connection - flash[:notice] = l(:notice_successful_connection) - rescue Exception => e - flash[:error] = l(:error_unable_to_connect, e.message) - end - redirect_to :action => 'index' - end - - def destroy - @auth_source = AuthSource.find(params[:id]) - unless @auth_source.users.find(:first) - @auth_source.destroy - flash[:notice] = l(:notice_successful_delete) - end - redirect_to :action => 'index' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1f7f86dc321cbe0fcb94878fe50d5b529000cd57.svn-base --- a/.svn/pristine/1f/1f7f86dc321cbe0fcb94878fe50d5b529000cd57.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,152 +0,0 @@ -* Exported the changelog of Pagination code for historical reference. - -* Imported some patches from Rails Trac (others closed as "wontfix"): - #8176, #7325, #7028, #4113. Documentation is much cleaner now and there - are some new unobtrusive features! - -* Extracted Pagination from Rails trunk (r6795) - -# -# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb -# -# Generated by Trac 0.10.3 -# 05/20/07 23:48:02 -# - -09/03/06 23:28:54 david [4953] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Docs and deprecation - -08/07/06 12:40:14 bitsweat [4715] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Deprecate direct usage of @params. Update ActionView::Base for - instance var deprecation. - -06/21/06 02:16:11 rick [4476] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Fix indent in pagination documentation. Closes #4990. [Kevin Clark] - -04/25/06 17:42:48 marcel [4268] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Remove all remaining references to @params in the documentation. - -03/16/06 06:38:08 rick [3899] - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - trivial documentation patch for #pagination_links [Francois - Beausoleil] closes #4258 - -02/20/06 03:15:22 david [3620] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/actionpack/test/activerecord/pagination_test.rb (modified) - * trunk/activerecord/CHANGELOG (modified) - * trunk/activerecord/lib/active_record/base.rb (modified) - * trunk/activerecord/test/base_test.rb (modified) - Added :count option to pagination that'll make it possible for the - ActiveRecord::Base.count call to using something else than * for the - count. Especially important for count queries using DISTINCT #3839 - [skaes]. Added :select option to Base.count that'll allow you to - select something else than * to be counted on. Especially important - for count queries using DISTINCT (closes #3839) [skaes]. - -02/09/06 09:17:40 nzkoz [3553] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/actionpack/test/active_record_unit.rb (added) - * trunk/actionpack/test/activerecord (added) - * trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added) - * trunk/actionpack/test/activerecord/pagination_test.rb (added) - * trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted) - * trunk/actionpack/test/fixtures/companies.yml (added) - * trunk/actionpack/test/fixtures/company.rb (added) - * trunk/actionpack/test/fixtures/db_definitions (added) - * trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added) - * trunk/actionpack/test/fixtures/developer.rb (added) - * trunk/actionpack/test/fixtures/developers_projects.yml (added) - * trunk/actionpack/test/fixtures/developers.yml (added) - * trunk/actionpack/test/fixtures/project.rb (added) - * trunk/actionpack/test/fixtures/projects.yml (added) - * trunk/actionpack/test/fixtures/replies.yml (added) - * trunk/actionpack/test/fixtures/reply.rb (added) - * trunk/actionpack/test/fixtures/topic.rb (added) - * trunk/actionpack/test/fixtures/topics.yml (added) - * Fix pagination problems when using include - * Introduce Unit Tests for pagination - * Allow count to work with :include by using count distinct. - - [Kevin Clark & Jeremy Hopple] - -11/05/05 02:10:29 bitsweat [2878] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Update paginator docs. Closes #2744. - -10/16/05 15:42:03 minam [2649] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Update/clean up AP documentation (rdoc) - -08/31/05 00:13:10 ulysses [2078] - * trunk/actionpack/CHANGELOG (modified) - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Add option to specify the singular name used by pagination. Closes - #1960 - -08/23/05 14:24:15 minam [2041] - * trunk/actionpack/CHANGELOG (modified) - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - Add support for :include with pagination (subject to existing - constraints for :include with :limit and :offset) #1478 - [michael@schubert.cx] - -07/15/05 20:27:38 david [1839] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - More pagination speed #1334 [Stefan Kaes] - -07/14/05 08:02:01 david [1832] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - * trunk/actionpack/test/controller/addresses_render_test.rb (modified) - Made pagination faster #1334 [Stefan Kaes] - -04/13/05 05:40:22 david [1159] - * trunk/actionpack/CHANGELOG (modified) - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/activerecord/lib/active_record/base.rb (modified) - Fixed pagination to work with joins #1034 [scott@sigkill.org] - -04/02/05 09:11:17 david [1067] - * trunk/actionpack/CHANGELOG (modified) - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/actionpack/lib/action_controller/scaffolding.rb (modified) - * trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified) - * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified) - * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified) - Added pagination for scaffolding (10 items per page) #964 - [mortonda@dgrmm.net] - -03/31/05 14:46:11 david [1048] - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - Improved the message display on the exception handler pages #963 - [Johan Sorensen] - -03/27/05 00:04:07 david [1017] - * trunk/actionpack/CHANGELOG (modified) - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - Fixed that pagination_helper would ignore :params #947 [Sebastian - Kanthak] - -03/22/05 13:09:44 david [976] - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - Fixed documentation and prepared for 0.11.0 release - -03/21/05 14:35:36 david [967] - * trunk/actionpack/lib/action_controller/pagination.rb (modified) - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) - Tweaked the documentation - -03/20/05 23:12:05 david [949] - * trunk/actionpack/CHANGELOG (modified) - * trunk/actionpack/lib/action_controller.rb (modified) - * trunk/actionpack/lib/action_controller/pagination.rb (added) - * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added) - * trunk/activesupport/lib/active_support/core_ext/kernel.rb (added) - Added pagination support through both a controller and helper add-on - #817 [Sam Stephenson] diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1fbae96ef6943c3bcd6afe5e00988dd05b87165c.svn-base --- a/.svn/pristine/1f/1fbae96ef6943c3bcd6afe5e00988dd05b87165c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 IssueStatusesController < ApplicationController - layout 'admin' - - before_filter :require_admin, :except => :index - before_filter :require_admin_or_api_request, :only => :index - accept_api_auth :index - - def index - respond_to do |format| - format.html { - @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position" - render :action => "index", :layout => false if request.xhr? - } - format.api { - @issue_statuses = IssueStatus.all(:order => 'position') - } - end - end - - def new - @issue_status = IssueStatus.new - end - - def create - @issue_status = IssueStatus.new(params[:issue_status]) - if request.post? && @issue_status.save - flash[:notice] = l(:notice_successful_create) - redirect_to :action => 'index' - else - render :action => 'new' - end - end - - def edit - @issue_status = IssueStatus.find(params[:id]) - end - - def update - @issue_status = IssueStatus.find(params[:id]) - if request.put? && @issue_status.update_attributes(params[:issue_status]) - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'index' - else - render :action => 'edit' - end - end - - def destroy - IssueStatus.find(params[:id]).destroy - redirect_to :action => 'index' - rescue - flash[:error] = l(:error_unable_delete_issue_status) - redirect_to :action => 'index' - end - - def update_issue_done_ratio - if request.post? && IssueStatus.update_issue_done_ratios - flash[:notice] = l(:notice_issue_done_ratios_updated) - else - flash[:error] = l(:error_issue_done_ratios_not_updated) - end - redirect_to :action => 'index' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1fc4113fdbf747b80464d17633b16c0ed0796efd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1fc4113fdbf747b80464d17633b16c0ed0796efd.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,7 @@ +

    <%=l(:label_report_plural)%>

    + +

    <%=@report_title%>

    +<%= render :partial => 'details', :locals => { :data => @data, :field_name => @field, :rows => @rows } %> +
    +<%= link_to l(:button_back), project_issues_report_path(@project) %> + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1fd3808f19058a6dd60b8c79e61039841f985687.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1fd3808f19058a6dd60b8c79e61039841f985687.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,211 @@ +# = Redmine configuration file +# +# Each environment has it's own configuration options. If you are only +# running in production, only the production block needs to be configured. +# Environment specific configuration options override the default ones. +# +# Note that this file needs to be a valid YAML file. +# DO NOT USE TABS! Use 2 spaces instead of tabs for identation. +# +# == Outgoing email settings (email_delivery setting) +# +# === Common configurations +# +# ==== Sendmail command +# +# production: +# email_delivery: +# delivery_method: :sendmail +# +# ==== Simple SMTP server at localhost +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# address: "localhost" +# port: 25 +# +# ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# address: "example.com" +# port: 25 +# authentication: :login +# domain: 'foo.com' +# user_name: 'myaccount' +# password: 'password' +# +# ==== SMTP server at example.com using PLAIN authentication +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# address: "example.com" +# port: 25 +# authentication: :plain +# domain: 'example.com' +# user_name: 'myaccount' +# password: 'password' +# +# ==== SMTP server at using TLS (GMail) +# +# This might require some additional configuration. See the guides at: +# http://www.redmine.org/projects/redmine/wiki/EmailConfiguration +# +# production: +# email_delivery: +# delivery_method: :smtp +# smtp_settings: +# enable_starttls_auto: true +# address: "smtp.gmail.com" +# port: 587 +# domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps +# authentication: :plain +# user_name: "your_email@gmail.com" +# password: "your_password" +# +# +# === More configuration options +# +# See the "Configuration options" at the following website for a list of the +# full options allowed: +# +# http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer + + +# default configuration options for all environments +default: + # Outgoing emails configuration (see examples above) + email_delivery: + delivery_method: :smtp + smtp_settings: + address: smtp.example.net + port: 25 + domain: example.net + authentication: :login + user_name: "redmine@example.net" + password: "redmine" + + # Absolute path to the directory where attachments are stored. + # The default is the 'files' directory in your Redmine instance. + # Your Redmine instance needs to have write permission on this + # directory. + # Examples: + # attachments_storage_path: /var/redmine/files + # attachments_storage_path: D:/redmine/files + attachments_storage_path: + + # Configuration of the autologin cookie. + # autologin_cookie_name: the name of the cookie (default: autologin) + # autologin_cookie_path: the cookie path (default: /) + # autologin_cookie_secure: true sets the cookie secure flag (default: false) + autologin_cookie_name: + autologin_cookie_path: + autologin_cookie_secure: + + # Configuration of SCM executable command. + # + # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe) + # On Windows + CRuby, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work. + # + # On Windows + JRuby 1.6.2, path which contains spaces does not work. + # For example, "C:\Program Files\TortoiseHg\hg.exe". + # If you want to this feature, you need to install to the path which does not contains spaces. + # For example, "C:\TortoiseHg\hg.exe". + # + # Examples: + # scm_subversion_command: svn # (default: svn) + # scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg) + # scm_git_command: /usr/local/bin/git # (default: git) + # scm_cvs_command: cvs # (default: cvs) + # scm_bazaar_command: bzr.exe # (default: bzr) + # scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs) + # + scm_subversion_command: + scm_mercurial_command: + scm_git_command: + scm_cvs_command: + scm_bazaar_command: + scm_darcs_command: + + # Absolute path to the SCM commands errors (stderr) log file. + # The default is to log in the 'log' directory of your Redmine instance. + # Example: + # scm_stderr_log_file: /var/log/redmine_scm_stderr.log + scm_stderr_log_file: + + # Key used to encrypt sensitive data in the database (SCM and LDAP passwords). + # If you don't want to enable data encryption, just leave it blank. + # WARNING: losing/changing this key will make encrypted data unreadable. + # + # If you want to encrypt existing passwords in your database: + # * set the cipher key here in your configuration file + # * encrypt data using 'rake db:encrypt RAILS_ENV=production' + # + # If you have encrypted data and want to change this key, you have to: + # * decrypt data using 'rake db:decrypt RAILS_ENV=production' first + # * change the cipher key here in your configuration file + # * encrypt data using 'rake db:encrypt RAILS_ENV=production' + database_cipher_key: + + # Set this to false to disable plugins' assets mirroring on startup. + # You can use `rake redmine:plugins:assets` to manually mirror assets + # to public/plugin_assets when you install/upgrade a Redmine plugin. + # + #mirror_plugins_assets_on_startup: false + + # Your secret key for verifying cookie session data integrity. If you + # change this key, all old sessions will become invalid! Make sure the + # secret is at least 30 characters and all random, no regular words or + # you'll be exposed to dictionary attacks. + # + # If you have a load-balancing Redmine cluster, you have to use the + # same secret token on each machine. + #secret_token: 'change it to a long random string' + + # Absolute path (e.g. /usr/bin/convert, c:/im/convert.exe) to + # the ImageMagick's `convert` binary. Used to generate attachment thumbnails. + #imagemagick_convert_command: + + # Configuration of RMagcik font. + # + # Redmine uses RMagcik in order to export gantt png. + # You don't need this setting if you don't install RMagcik. + # + # In CJK (Chinese, Japanese and Korean), + # in order to show CJK characters correctly, + # you need to set this configuration. + # + # Because there is no standard font across platforms in CJK, + # you need to set a font installed in your server. + # + # This setting is not necessary in non CJK. + # + # Examples for Japanese: + # Windows: + # rmagick_font_path: C:\windows\fonts\msgothic.ttc + # Linux: + # rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf + # + rmagick_font_path: + + # Maximum number of simultaneous AJAX uploads + #max_concurrent_ajax_uploads: 2 + + # Configure OpenIdAuthentication.store + # + # allowed values: :memory, :file, :memcache + #openid_authentication_store: :memory + +# specific configuration options for production environment +# that overrides the default ones +production: + +# specific configuration options for development environment +# that overrides the default ones +development: diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1fe3164d40b2f175bf0aa2fdbd0804dbea4969a1.svn-base --- a/.svn/pristine/1f/1fe3164d40b2f175bf0aa2fdbd0804dbea4969a1.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,578 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'projects_controller' - -# Re-raise errors caught by the controller. -class ProjectsController; def rescue_action(e) raise e end; end - -class ProjectsControllerTest < ActionController::TestCase - fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, - :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, - :attachments, :custom_fields, :custom_values, :time_entries - - def setup - @controller = ProjectsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - @request.session[:user_id] = nil - Setting.default_language = 'en' - end - - def test_index - get :index - assert_response :success - assert_template 'index' - assert_not_nil assigns(:projects) - - assert_tag :ul, :child => {:tag => 'li', - :descendant => {:tag => 'a', :content => 'eCookbook'}, - :child => { :tag => 'ul', - :descendant => { :tag => 'a', - :content => 'Child of private child' - } - } - } - - assert_no_tag :a, :content => /Private child of eCookbook/ - end - - def test_index_atom - get :index, :format => 'atom' - assert_response :success - assert_template 'common/feed' - assert_select 'feed>title', :text => 'Redmine: Latest projects' - assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_condition(User.current)) - end - - context "#index" do - context "by non-admin user with view_time_entries permission" do - setup do - @request.session[:user_id] = 3 - end - should "show overall spent time link" do - get :index - assert_template 'index' - assert_tag :a, :attributes => {:href => '/time_entries'} - end - end - - context "by non-admin user without view_time_entries permission" do - setup do - Role.find(2).remove_permission! :view_time_entries - Role.non_member.remove_permission! :view_time_entries - Role.anonymous.remove_permission! :view_time_entries - @request.session[:user_id] = 3 - end - should "not show overall spent time link" do - get :index - assert_template 'index' - assert_no_tag :a, :attributes => {:href => '/time_entries'} - end - end - end - - context "#new" do - context "by admin user" do - setup do - @request.session[:user_id] = 1 - end - - should "accept get" do - get :new - assert_response :success - assert_template 'new' - end - - end - - context "by non-admin user with add_project permission" do - setup do - Role.non_member.add_permission! :add_project - @request.session[:user_id] = 9 - end - - should "accept get" do - get :new - assert_response :success - assert_template 'new' - assert_no_tag :select, :attributes => {:name => 'project[parent_id]'} - end - end - - context "by non-admin user with add_subprojects permission" do - setup do - Role.find(1).remove_permission! :add_project - Role.find(1).add_permission! :add_subprojects - @request.session[:user_id] = 2 - end - - should "accept get" do - get :new, :parent_id => 'ecookbook' - assert_response :success - assert_template 'new' - # parent project selected - assert_tag :select, :attributes => {:name => 'project[parent_id]'}, - :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}} - # no empty value - assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}, - :child => {:tag => 'option', :attributes => {:value => ''}} - end - end - - end - - context "POST :create" do - context "by admin user" do - setup do - @request.session[:user_id] = 1 - end - - should "create a new project" do - post :create, - :project => { - :name => "blog", - :description => "weblog", - :homepage => 'http://weblog', - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' }, - :tracker_ids => ['1', '3'], - # an issue custom field that is not for all project - :issue_custom_field_ids => ['9'], - :enabled_module_names => ['issue_tracking', 'news', 'repository'] - } - assert_redirected_to '/projects/blog/settings' - - project = Project.find_by_name('blog') - assert_kind_of Project, project - assert project.active? - assert_equal 'weblog', project.description - assert_equal 'http://weblog', project.homepage - assert_equal true, project.is_public? - assert_nil project.parent - assert_equal 'Beta', project.custom_value_for(3).value - assert_equal [1, 3], project.trackers.map(&:id).sort - assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort - assert project.issue_custom_fields.include?(IssueCustomField.find(9)) - end - - should "create a new subproject" do - post :create, :project => { :name => "blog", - :description => "weblog", - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' }, - :parent_id => 1 - } - assert_redirected_to '/projects/blog/settings' - - project = Project.find_by_name('blog') - assert_kind_of Project, project - assert_equal Project.find(1), project.parent - end - - should "continue" do - assert_difference 'Project.count' do - post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue' - end - assert_redirected_to '/projects/new?' - end - end - - context "by non-admin user with add_project permission" do - setup do - Role.non_member.add_permission! :add_project - @request.session[:user_id] = 9 - end - - should "accept create a Project" do - post :create, :project => { :name => "blog", - :description => "weblog", - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' }, - :tracker_ids => ['1', '3'], - :enabled_module_names => ['issue_tracking', 'news', 'repository'] - } - - assert_redirected_to '/projects/blog/settings' - - project = Project.find_by_name('blog') - assert_kind_of Project, project - assert_equal 'weblog', project.description - assert_equal true, project.is_public? - assert_equal [1, 3], project.trackers.map(&:id).sort - assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort - - # User should be added as a project member - assert User.find(9).member_of?(project) - assert_equal 1, project.members.size - end - - should "fail with parent_id" do - assert_no_difference 'Project.count' do - post :create, :project => { :name => "blog", - :description => "weblog", - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' }, - :parent_id => 1 - } - end - assert_response :success - project = assigns(:project) - assert_kind_of Project, project - assert_not_nil project.errors[:parent_id] - end - end - - context "by non-admin user with add_subprojects permission" do - setup do - Role.find(1).remove_permission! :add_project - Role.find(1).add_permission! :add_subprojects - @request.session[:user_id] = 2 - end - - should "create a project with a parent_id" do - post :create, :project => { :name => "blog", - :description => "weblog", - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' }, - :parent_id => 1 - } - assert_redirected_to '/projects/blog/settings' - project = Project.find_by_name('blog') - end - - should "fail without parent_id" do - assert_no_difference 'Project.count' do - post :create, :project => { :name => "blog", - :description => "weblog", - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' } - } - end - assert_response :success - project = assigns(:project) - assert_kind_of Project, project - assert_not_nil project.errors[:parent_id] - end - - should "fail with unauthorized parent_id" do - assert !User.find(2).member_of?(Project.find(6)) - assert_no_difference 'Project.count' do - post :create, :project => { :name => "blog", - :description => "weblog", - :identifier => "blog", - :is_public => 1, - :custom_field_values => { '3' => 'Beta' }, - :parent_id => 6 - } - end - assert_response :success - project = assigns(:project) - assert_kind_of Project, project - assert_not_nil project.errors[:parent_id] - end - end - end - - def test_create_should_preserve_modules_on_validation_failure - with_settings :default_projects_modules => ['issue_tracking', 'repository'] do - @request.session[:user_id] = 1 - assert_no_difference 'Project.count' do - post :create, :project => { - :name => "blog", - :identifier => "", - :enabled_module_names => %w(issue_tracking news) - } - end - assert_response :success - project = assigns(:project) - assert_equal %w(issue_tracking news), project.enabled_module_names.sort - end - end - - def test_show_by_id - get :show, :id => 1 - assert_response :success - assert_template 'show' - assert_not_nil assigns(:project) - end - - def test_show_by_identifier - get :show, :id => 'ecookbook' - assert_response :success - assert_template 'show' - assert_not_nil assigns(:project) - assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) - - assert_tag 'li', :content => /Development status/ - end - - def test_show_should_not_display_hidden_custom_fields - ProjectCustomField.find_by_name('Development status').update_attribute :visible, false - get :show, :id => 'ecookbook' - assert_response :success - assert_template 'show' - assert_not_nil assigns(:project) - - assert_no_tag 'li', :content => /Development status/ - end - - def test_show_should_not_fail_when_custom_values_are_nil - project = Project.find_by_identifier('ecookbook') - project.custom_values.first.update_attribute(:value, nil) - get :show, :id => 'ecookbook' - assert_response :success - assert_template 'show' - assert_not_nil assigns(:project) - assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) - end - - def show_archived_project_should_be_denied - project = Project.find_by_identifier('ecookbook') - project.archive! - - get :show, :id => 'ecookbook' - assert_response 403 - assert_nil assigns(:project) - assert_tag :tag => 'p', :content => /archived/ - end - - def test_private_subprojects_hidden - get :show, :id => 'ecookbook' - assert_response :success - assert_template 'show' - assert_no_tag :tag => 'a', :content => /Private child/ - end - - def test_private_subprojects_visible - @request.session[:user_id] = 2 # manager who is a member of the private subproject - get :show, :id => 'ecookbook' - assert_response :success - assert_template 'show' - assert_tag :tag => 'a', :content => /Private child/ - end - - def test_settings - @request.session[:user_id] = 2 # manager - get :settings, :id => 1 - assert_response :success - assert_template 'settings' - end - - def test_settings_should_be_denied_for_member_on_closed_project - Project.find(1).close - @request.session[:user_id] = 2 # manager - - get :settings, :id => 1 - assert_response 403 - end - - def test_settings_should_be_denied_for_anonymous_on_closed_project - Project.find(1).close - - get :settings, :id => 1 - assert_response 302 - end - - def test_update - @request.session[:user_id] = 2 # manager - post :update, :id => 1, :project => {:name => 'Test changed name', - :issue_custom_field_ids => ['']} - assert_redirected_to '/projects/ecookbook/settings' - project = Project.find(1) - assert_equal 'Test changed name', project.name - end - - def test_update_with_failure - @request.session[:user_id] = 2 # manager - post :update, :id => 1, :project => {:name => ''} - assert_response :success - assert_template 'settings' - assert_error_tag :content => /name can't be blank/i - end - - def test_update_should_be_denied_for_member_on_closed_project - Project.find(1).close - @request.session[:user_id] = 2 # manager - - post :update, :id => 1, :project => {:name => 'Closed'} - assert_response 403 - assert_equal 'eCookbook', Project.find(1).name - end - - def test_update_should_be_denied_for_anonymous_on_closed_project - Project.find(1).close - - post :update, :id => 1, :project => {:name => 'Closed'} - assert_response 302 - assert_equal 'eCookbook', Project.find(1).name - end - - def test_modules - @request.session[:user_id] = 2 - Project.find(1).enabled_module_names = ['issue_tracking', 'news'] - - post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents'] - assert_redirected_to '/projects/ecookbook/settings/modules' - assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort - end - - def test_destroy_without_confirmation - @request.session[:user_id] = 1 # admin - delete :destroy, :id => 1 - assert_response :success - assert_template 'destroy' - assert_not_nil Project.find_by_id(1) - assert_tag :tag => 'strong', - :content => ['Private child of eCookbook', - 'Child of private child, eCookbook Subproject 1', - 'eCookbook Subproject 2'].join(', ') - end - - def test_destroy - @request.session[:user_id] = 1 # admin - delete :destroy, :id => 1, :confirm => 1 - assert_redirected_to '/admin/projects' - assert_nil Project.find_by_id(1) - end - - def test_archive - @request.session[:user_id] = 1 # admin - post :archive, :id => 1 - assert_redirected_to '/admin/projects' - assert !Project.find(1).active? - end - - def test_archive_with_failure - @request.session[:user_id] = 1 - Project.any_instance.stubs(:archive).returns(false) - post :archive, :id => 1 - assert_redirected_to '/admin/projects' - assert_match /project cannot be archived/i, flash[:error] - end - - def test_unarchive - @request.session[:user_id] = 1 # admin - Project.find(1).archive - post :unarchive, :id => 1 - assert_redirected_to '/admin/projects' - assert Project.find(1).active? - end - - def test_close - @request.session[:user_id] = 2 - post :close, :id => 1 - assert_redirected_to '/projects/ecookbook' - assert_equal Project::STATUS_CLOSED, Project.find(1).status - end - - def test_reopen - Project.find(1).close - @request.session[:user_id] = 2 - post :reopen, :id => 1 - assert_redirected_to '/projects/ecookbook' - assert Project.find(1).active? - end - - def test_project_breadcrumbs_should_be_limited_to_3_ancestors - CustomField.delete_all - parent = nil - 6.times do |i| - p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}") - p.set_parent!(parent) - get :show, :id => p - assert_tag :h1, :parent => { :attributes => {:id => 'header'}}, - :children => { :count => [i, 3].min, - :only => { :tag => 'a' } } - - parent = p - end - end - - def test_get_copy - @request.session[:user_id] = 1 # admin - get :copy, :id => 1 - assert_response :success - assert_template 'copy' - assert assigns(:project) - assert_equal Project.find(1).description, assigns(:project).description - assert_nil assigns(:project).id - - assert_tag :tag => 'input', - :attributes => {:name => 'project[enabled_module_names][]', :value => 'issue_tracking'} - end - - def test_get_copy_with_invalid_source_should_respond_with_404 - @request.session[:user_id] = 1 - get :copy, :id => 99 - assert_response 404 - end - - def test_post_copy_should_copy_requested_items - @request.session[:user_id] = 1 # admin - CustomField.delete_all - - assert_difference 'Project.count' do - post :copy, :id => 1, - :project => { - :name => 'Copy', - :identifier => 'unique-copy', - :tracker_ids => ['1', '2', '3', ''], - :enabled_module_names => %w(issue_tracking time_tracking) - }, - :only => %w(issues versions) - end - project = Project.find('unique-copy') - source = Project.find(1) - assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort - - assert_equal source.versions.count, project.versions.count, "All versions were not copied" - assert_equal source.issues.count, project.issues.count, "All issues were not copied" - assert_equal 0, project.members.count - end - - def test_post_copy_should_redirect_to_settings_when_successful - @request.session[:user_id] = 1 # admin - post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'} - assert_response :redirect - assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy' - end - - def test_jump_should_redirect_to_active_tab - get :show, :id => 1, :jump => 'issues' - assert_redirected_to '/projects/ecookbook/issues' - end - - def test_jump_should_not_redirect_to_inactive_tab - get :show, :id => 3, :jump => 'documents' - assert_response :success - assert_template 'show' - end - - def test_jump_should_not_redirect_to_unknown_tab - get :show, :id => 3, :jump => 'foobar' - assert_response :success - assert_template 'show' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/1f/1feeecc995893880348eca32af1a4a9f7f1dfb97.svn-base --- a/.svn/pristine/1f/1feeecc995893880348eca32af1a4a9f7f1dfb97.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' - -desc 'Default: run unit tests.' -task :default => :test - -desc 'Test the classic_pagination plugin.' -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' - t.verbose = true -end - -desc 'Generate documentation for the classic_pagination plugin.' -Rake::RDocTask.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'Pagination' - rdoc.options << '--line-numbers' << '--inline-source' - rdoc.rdoc_files.include('README') - rdoc.rdoc_files.include('lib/**/*.rb') -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/2006d03f57217f185b674373ebf261dbb0c3c839.svn-base --- a/.svn/pristine/20/2006d03f57217f185b674373ebf261dbb0c3c839.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,200 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ApiTest::MembershipsTest < ActionController::IntegrationTest - fixtures :projects, :users, :roles, :members, :member_roles - - def setup - Setting.rest_api_enabled = '1' - end - - context "/projects/:project_id/memberships" do - context "GET" do - context "xml" do - should "return memberships" do - get '/projects/1/memberships.xml', {}, credentials('jsmith') - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'memberships', - :attributes => {:type => 'array'}, - :child => { - :tag => 'membership', - :child => { - :tag => 'id', - :content => '2', - :sibling => { - :tag => 'user', - :attributes => {:id => '3', :name => 'Dave Lopper'}, - :sibling => { - :tag => 'roles', - :child => { - :tag => 'role', - :attributes => {:id => '2', :name => 'Developer'} - } - } - } - } - } - end - end - - context "json" do - should "return memberships" do - get '/projects/1/memberships.json', {}, credentials('jsmith') - - assert_response :success - assert_equal 'application/json', @response.content_type - json = ActiveSupport::JSON.decode(response.body) - assert_equal({ - "memberships" => - [{"id"=>1, - "project" => {"name"=>"eCookbook", "id"=>1}, - "roles" => [{"name"=>"Manager", "id"=>1}], - "user" => {"name"=>"John Smith", "id"=>2}}, - {"id"=>2, - "project" => {"name"=>"eCookbook", "id"=>1}, - "roles" => [{"name"=>"Developer", "id"=>2}], - "user" => {"name"=>"Dave Lopper", "id"=>3}}], - "limit" => 25, - "total_count" => 2, - "offset" => 0}, - json) - end - end - end - - context "POST" do - context "xml" do - should "create membership" do - assert_difference 'Member.count' do - post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith') - - assert_response :created - end - end - - should "return errors on failure" do - assert_no_difference 'Member.count' do - post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith') - - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"} - end - end - end - end - end - - context "/memberships/:id" do - context "GET" do - context "xml" do - should "return the membership" do - get '/memberships/2.xml', {}, credentials('jsmith') - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'membership', - :child => { - :tag => 'id', - :content => '2', - :sibling => { - :tag => 'user', - :attributes => {:id => '3', :name => 'Dave Lopper'}, - :sibling => { - :tag => 'roles', - :child => { - :tag => 'role', - :attributes => {:id => '2', :name => 'Developer'} - } - } - } - } - end - end - - context "json" do - should "return the membership" do - get '/memberships/2.json', {}, credentials('jsmith') - - assert_response :success - assert_equal 'application/json', @response.content_type - json = ActiveSupport::JSON.decode(response.body) - assert_equal( - {"membership" => { - "id" => 2, - "project" => {"name"=>"eCookbook", "id"=>1}, - "roles" => [{"name"=>"Developer", "id"=>2}], - "user" => {"name"=>"Dave Lopper", "id"=>3}} - }, - json) - end - end - end - - context "PUT" do - context "xml" do - should "update membership" do - assert_not_equal [1,2], Member.find(2).role_ids.sort - assert_no_difference 'Member.count' do - put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith') - - assert_response :ok - assert_equal '', @response.body - end - member = Member.find(2) - assert_equal [1,2], member.role_ids.sort - end - - should "return errors on failure" do - put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith') - - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/} - end - end - end - - context "DELETE" do - context "xml" do - should "destroy membership" do - assert_difference 'Member.count', -1 do - delete '/memberships/2.xml', {}, credentials('jsmith') - - assert_response :ok - assert_equal '', @response.body - end - assert_nil Member.find_by_id(2) - end - - should "respond with 422 on failure" do - assert_no_difference 'Member.count' do - # A membership with an inherited role can't be deleted - Member.find(2).member_roles.first.update_attribute :inherited_from, 99 - delete '/memberships/2.xml', {}, credentials('jsmith') - - assert_response :unprocessable_entity - end - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/201b3871255841575db5d12d8a5ed0f4a9dfd618.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/201b3871255841575db5d12d8a5ed0f4a9dfd618.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,88 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldsController < ApplicationController + layout 'admin' + + before_filter :require_admin + before_filter :build_new_custom_field, :only => [:new, :create] + before_filter :find_custom_field, :only => [:edit, :update, :destroy] + accept_api_auth :index + + def index + respond_to do |format| + format.html { + @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name } + } + format.api { + @custom_fields = CustomField.all + } + end + end + + def new + @custom_field.field_format = 'string' if @custom_field.field_format.blank? + @custom_field.default_value = nil + end + + def create + if @custom_field.save + flash[:notice] = l(:notice_successful_create) + call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field) + redirect_to custom_fields_path(:tab => @custom_field.class.name) + else + render :action => 'new' + end + end + + def edit + end + + def update + if @custom_field.update_attributes(params[:custom_field]) + flash[:notice] = l(:notice_successful_update) + call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field) + redirect_to custom_fields_path(:tab => @custom_field.class.name) + else + render :action => 'edit' + end + end + + def destroy + begin + @custom_field.destroy + rescue + flash[:error] = l(:error_can_not_delete_custom_field) + end + redirect_to custom_fields_path(:tab => @custom_field.class.name) + end + + private + + def build_new_custom_field + @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field]) + if @custom_field.nil? + render :action => 'select_type' + end + end + + def find_custom_field + @custom_field = CustomField.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/201ba8bb33fe7d932e25d4d032482548144aca3a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/201ba8bb33fe7d932e25d4d032482548144aca3a.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,412 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'redmine/scm/adapters/abstract_adapter' + +module Redmine + module Scm + module Adapters + class GitAdapter < AbstractAdapter + + # Git executable name + GIT_BIN = Redmine::Configuration['scm_git_command'] || "git" + + class GitBranch < Branch + attr_accessor :is_default + end + + class << self + def client_command + @@bin ||= GIT_BIN + end + + def sq_bin + @@sq_bin ||= shell_quote_command + end + + def client_version + @@client_version ||= (scm_command_version || []) + end + + def client_available + !client_version.empty? + end + + def scm_command_version + scm_version = scm_version_from_command_line.dup + if scm_version.respond_to?(:force_encoding) + scm_version.force_encoding('ASCII-8BIT') + end + if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}) + m[2].scan(%r{\d+}).collect(&:to_i) + end + end + + def scm_version_from_command_line + shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s + end + end + + def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) + super + @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding + end + + def path_encoding + @path_encoding + end + + def info + begin + Info.new(:root_url => url, :lastrev => lastrev('',nil)) + rescue + nil + end + end + + def branches + return @branches if @branches + @branches = [] + cmd_args = %w|branch --no-color --verbose --no-abbrev| + git_cmd(cmd_args) do |io| + io.each_line do |line| + branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$') + bran = GitBranch.new(branch_rev[2]) + bran.revision = branch_rev[3] + bran.scmid = branch_rev[3] + bran.is_default = ( branch_rev[1] == '*' ) + @branches << bran + end + end + @branches.sort! + rescue ScmCommandAborted + nil + end + + def tags + return @tags if @tags + cmd_args = %w|tag| + git_cmd(cmd_args) do |io| + @tags = io.readlines.sort!.map{|t| t.strip} + end + rescue ScmCommandAborted + nil + end + + def default_branch + bras = self.branches + return nil if bras.nil? + default_bras = bras.select{|x| x.is_default == true} + return default_bras.first.to_s if ! default_bras.empty? + master_bras = bras.select{|x| x.to_s == 'master'} + master_bras.empty? ? bras.first.to_s : 'master' + end + + def entry(path=nil, identifier=nil) + parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?} + search_path = parts[0..-2].join('/') + search_name = parts[-1] + if search_path.blank? && search_name.blank? + # Root entry + Entry.new(:path => '', :kind => 'dir') + else + # Search for the entry in the parent directory + es = entries(search_path, identifier, + options = {:report_last_commit => false}) + es ? es.detect {|e| e.name == search_name} : nil + end + end + + def entries(path=nil, identifier=nil, options={}) + path ||= '' + p = scm_iconv(@path_encoding, 'UTF-8', path) + entries = Entries.new + cmd_args = %w|ls-tree -l| + cmd_args << "HEAD:#{p}" if identifier.nil? + cmd_args << "#{identifier}:#{p}" if identifier + git_cmd(cmd_args) do |io| + io.each_line do |line| + e = line.chomp.to_s + if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/ + type = $1 + sha = $2 + size = $3 + name = $4 + if name.respond_to?(:force_encoding) + name.force_encoding(@path_encoding) + end + full_path = p.empty? ? name : "#{p}/#{name}" + n = scm_iconv('UTF-8', @path_encoding, name) + full_p = scm_iconv('UTF-8', @path_encoding, full_path) + entries << Entry.new({:name => n, + :path => full_p, + :kind => (type == "tree") ? 'dir' : 'file', + :size => (type == "tree") ? nil : size, + :lastrev => options[:report_last_commit] ? + lastrev(full_path, identifier) : Revision.new + }) unless entries.detect{|entry| entry.name == name} + end + end + end + entries.sort_by_name + rescue ScmCommandAborted + nil + end + + def lastrev(path, rev) + return nil if path.nil? + cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1| + cmd_args << rev if rev + cmd_args << "--" << path unless path.empty? + lines = [] + git_cmd(cmd_args) { |io| lines = io.readlines } + begin + id = lines[0].split[1] + author = lines[1].match('Author:\s+(.*)$')[1] + time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]) + + Revision.new({ + :identifier => id, + :scmid => id, + :author => author, + :time => time, + :message => nil, + :paths => nil + }) + rescue NoMethodError => e + logger.error("The revision '#{path}' has a wrong format") + return nil + end + rescue ScmCommandAborted + nil + end + + def revisions(path, identifier_from, identifier_to, options={}) + revs = Revisions.new + cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin| + cmd_args << "--reverse" if options[:reverse] + cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit] + cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty? + revisions = [] + if identifier_from || identifier_to + revisions << "" + revisions[0] << "#{identifier_from}.." if identifier_from + revisions[0] << "#{identifier_to}" if identifier_to + else + unless options[:includes].blank? + revisions += options[:includes] + end + unless options[:excludes].blank? + revisions += options[:excludes].map{|r| "^#{r}"} + end + end + + git_cmd(cmd_args, {:write_stdin => true}) do |io| + io.binmode + io.puts(revisions.join("\n")) + io.close_write + files=[] + changeset = {} + parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files + + io.each_line do |line| + if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/ + key = "commit" + value = $1 + parents_str = $2 + if (parsing_descr == 1 || parsing_descr == 2) + parsing_descr = 0 + revision = Revision.new({ + :identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => files, + :parents => changeset[:parents] + }) + if block_given? + yield revision + else + revs << revision + end + changeset = {} + files = [] + end + changeset[:commit] = $1 + unless parents_str.nil? or parents_str == "" + changeset[:parents] = parents_str.strip.split(' ') + end + elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/ + key = $1 + value = $2 + if key == "Author" + changeset[:author] = value + elsif key == "CommitDate" + changeset[:date] = value + end + elsif (parsing_descr == 0) && line.chomp.to_s == "" + parsing_descr = 1 + changeset[:description] = "" + elsif (parsing_descr == 1 || parsing_descr == 2) \ + && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/ + parsing_descr = 2 + fileaction = $1 + filepath = $2 + p = scm_iconv('UTF-8', @path_encoding, filepath) + files << {:action => fileaction, :path => p} + elsif (parsing_descr == 1 || parsing_descr == 2) \ + && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/ + parsing_descr = 2 + fileaction = $1 + filepath = $3 + p = scm_iconv('UTF-8', @path_encoding, filepath) + files << {:action => fileaction, :path => p} + elsif (parsing_descr == 1) && line.chomp.to_s == "" + parsing_descr = 2 + elsif (parsing_descr == 1) + changeset[:description] << line[4..-1] + end + end + + if changeset[:commit] + revision = Revision.new({ + :identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => files, + :parents => changeset[:parents] + }) + if block_given? + yield revision + else + revs << revision + end + end + end + revs + rescue ScmCommandAborted => e + err_msg = "git log error: #{e.message}" + logger.error(err_msg) + if block_given? + raise CommandFailed, err_msg + else + revs + end + end + + def diff(path, identifier_from, identifier_to=nil) + path ||= '' + cmd_args = [] + if identifier_to + cmd_args << "diff" << "--no-color" << identifier_to << identifier_from + else + cmd_args << "show" << "--no-color" << identifier_from + end + cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty? + diff = [] + git_cmd(cmd_args) do |io| + io.each_line do |line| + diff << line + end + end + diff + rescue ScmCommandAborted + nil + end + + def annotate(path, identifier=nil) + identifier = 'HEAD' if identifier.blank? + cmd_args = %w|blame --encoding=UTF-8| + cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path) + blame = Annotate.new + content = nil + git_cmd(cmd_args) { |io| io.binmode; content = io.read } + # git annotates binary files + return nil if content.is_binary_data? + identifier = '' + # git shows commit author on the first occurrence only + authors_by_commit = {} + content.split("\n").each do |line| + if line =~ /^([0-9a-f]{39,40})\s.*/ + identifier = $1 + elsif line =~ /^author (.+)/ + authors_by_commit[identifier] = $1.strip + elsif line =~ /^\t(.*)/ + blame.add_line($1, Revision.new( + :identifier => identifier, + :revision => identifier, + :scmid => identifier, + :author => authors_by_commit[identifier] + )) + identifier = '' + author = '' + end + end + blame + rescue ScmCommandAborted + nil + end + + def cat(path, identifier=nil) + if identifier.nil? + identifier = 'HEAD' + end + cmd_args = %w|show --no-color| + cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}" + cat = nil + git_cmd(cmd_args) do |io| + io.binmode + cat = io.read + end + cat + rescue ScmCommandAborted + nil + end + + class Revision < Redmine::Scm::Adapters::Revision + # Returns the readable identifier + def format_identifier + identifier[0,8] + end + end + + def git_cmd(args, options = {}, &block) + repo_path = root_url || url + full_args = ['--git-dir', repo_path] + if self.class.client_version_above?([1, 7, 2]) + full_args << '-c' << 'core.quotepath=false' + full_args << '-c' << 'log.decorate=no' + end + full_args += args + ret = shellout( + self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '), + options, + &block + ) + if $? && $?.exitstatus != 0 + raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}" + end + ret + end + private :git_cmd + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/2048f249df3ed3cda47a1ab8ba2f127935c58466.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/2048f249df3ed3cda47a1ab8ba2f127935c58466.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,248 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 MyControllerTest < ActionController::TestCase + fixtures :users, :user_preferences, :roles, :projects, :members, :member_roles, + :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources + + def setup + @request.session[:user_id] = 2 + end + + def test_index + get :index + assert_response :success + assert_template 'page' + end + + def test_page + get :page + assert_response :success + assert_template 'page' + end + + def test_page_with_timelog_block + preferences = User.find(2).pref + preferences[:my_page_layout] = {'top' => ['timelog']} + preferences.save! + TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10) + + get :page + assert_response :success + assert_select 'tr.time-entry' do + assert_select 'td.subject a[href=/issues/1]' + assert_select 'td.hours', :text => '2.50' + end + end + + def test_page_with_all_blocks + blocks = MyController::BLOCKS.keys + preferences = User.find(2).pref + preferences[:my_page_layout] = {'top' => blocks} + preferences.save! + + get :page + assert_response :success + assert_select 'div.mypage-box', blocks.size + end + + def test_my_account_should_show_editable_custom_fields + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + + assert_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} + end + + def test_my_account_should_not_show_non_editable_custom_fields + UserCustomField.find(4).update_attribute :editable, false + + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + + assert_no_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} + end + + def test_update_account + post :account, + :user => { + :firstname => "Joe", + :login => "root", + :admin => 1, + :group_ids => ['10'], + :custom_field_values => {"4" => "0100562500"} + } + + assert_redirected_to '/my/account' + user = User.find(2) + assert_equal user, assigns(:user) + assert_equal "Joe", user.firstname + assert_equal "jsmith", user.login + assert_equal "0100562500", user.custom_value_for(4).value + # ignored + assert !user.admin? + assert user.groups.empty? + end + + def test_my_account_should_show_destroy_link + get :account + assert_select 'a[href=/my/account/destroy]' + end + + def test_get_destroy_should_display_the_destroy_confirmation + get :destroy + assert_response :success + assert_template 'destroy' + assert_select 'form[action=/my/account/destroy]' do + assert_select 'input[name=confirm]' + end + end + + def test_post_destroy_without_confirmation_should_not_destroy_account + assert_no_difference 'User.count' do + post :destroy + end + assert_response :success + assert_template 'destroy' + end + + def test_post_destroy_without_confirmation_should_destroy_account + assert_difference 'User.count', -1 do + post :destroy, :confirm => '1' + end + assert_redirected_to '/' + assert_match /deleted/i, flash[:notice] + end + + def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account + User.any_instance.stubs(:own_account_deletable?).returns(false) + + assert_no_difference 'User.count' do + post :destroy, :confirm => '1' + end + assert_redirected_to '/my/account' + end + + def test_change_password + get :password + assert_response :success + assert_template 'password' + + # non matching password confirmation + post :password, :password => 'jsmith', + :new_password => 'secret123', + :new_password_confirmation => 'secret1234' + assert_response :success + assert_template 'password' + assert_error_tag :content => /Password doesn't match confirmation/ + + # wrong password + post :password, :password => 'wrongpassword', + :new_password => 'secret123', + :new_password_confirmation => 'secret123' + assert_response :success + assert_template 'password' + assert_equal 'Wrong password', flash[:error] + + # good password + post :password, :password => 'jsmith', + :new_password => 'secret123', + :new_password_confirmation => 'secret123' + assert_redirected_to '/my/account' + assert User.try_to_login('jsmith', 'secret123') + end + + def test_change_password_should_redirect_if_user_cannot_change_its_password + User.find(2).update_attribute(:auth_source_id, 1) + + get :password + assert_not_nil flash[:error] + assert_redirected_to '/my/account' + end + + def test_page_layout + get :page_layout + assert_response :success + assert_template 'page_layout' + end + + def test_add_block + post :add_block, :block => 'issuesreportedbyme' + assert_redirected_to '/my/page_layout' + assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme') + end + + def test_add_invalid_block_should_redirect + post :add_block, :block => 'invalid' + assert_redirected_to '/my/page_layout' + end + + def test_remove_block + post :remove_block, :block => 'issuesassignedtome' + assert_redirected_to '/my/page_layout' + assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome') + end + + def test_order_blocks + xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews'] + assert_response :success + assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left'] + end + + def test_reset_rss_key_with_existing_key + @previous_token_value = User.find(2).rss_key # Will generate one if it's missing + post :reset_rss_key + + assert_not_equal @previous_token_value, User.find(2).rss_key + assert User.find(2).rss_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_rss_key_without_existing_key + assert_nil User.find(2).rss_token + post :reset_rss_key + + assert User.find(2).rss_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_api_key_with_existing_key + @previous_token_value = User.find(2).api_key # Will generate one if it's missing + post :reset_api_key + + assert_not_equal @previous_token_value, User.find(2).api_key + assert User.find(2).api_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_api_key_without_existing_key + assert_nil User.find(2).api_token + post :reset_api_key + + assert User.find(2).api_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/205de64e23e7f44f01ee402d6427c247c8347982.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/205de64e23e7f44f01ee402d6427c247c8347982.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,6 @@ +<%= title [l(@enumeration.option_name), enumerations_path], @enumeration.name %> + +<%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/205eb718e22b02ea59cca1358f48bb87e5bd7f8d.svn-base --- a/.svn/pristine/20/205eb718e22b02ea59cca1358f48bb87e5bd7f8d.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,280 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'iconv' - -class Changeset < ActiveRecord::Base - belongs_to :repository - belongs_to :user - has_many :filechanges, :class_name => 'Change', :dependent => :delete_all - has_and_belongs_to_many :issues - has_and_belongs_to_many :parents, - :class_name => "Changeset", - :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", - :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id' - has_and_belongs_to_many :children, - :class_name => "Changeset", - :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", - :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id' - - acts_as_event :title => Proc.new {|o| o.title}, - :description => :long_comments, - :datetime => :committed_on, - :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}} - - acts_as_searchable :columns => 'comments', - :include => {:repository => :project}, - :project_key => "#{Repository.table_name}.project_id", - :date_column => 'committed_on' - - acts_as_activity_provider :timestamp => "#{table_name}.committed_on", - :author_key => :user_id, - :find_options => {:include => [:user, {:repository => :project}]} - - validates_presence_of :repository_id, :revision, :committed_on, :commit_date - validates_uniqueness_of :revision, :scope => :repository_id - validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true - - scope :visible, - lambda {|*args| { :include => {:repository => :project}, - :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } - - after_create :scan_for_issues - before_create :before_create_cs - - def revision=(r) - write_attribute :revision, (r.nil? ? nil : r.to_s) - end - - # Returns the identifier of this changeset; depending on repository backends - def identifier - if repository.class.respond_to? :changeset_identifier - repository.class.changeset_identifier self - else - revision.to_s - end - end - - def committed_on=(date) - self.commit_date = date - super - end - - # Returns the readable identifier - def format_identifier - if repository.class.respond_to? :format_changeset_identifier - repository.class.format_changeset_identifier self - else - identifier - end - end - - def project - repository.project - end - - def author - user || committer.to_s.split('<').first - end - - def before_create_cs - self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) - self.comments = self.class.normalize_comments( - self.comments, repository.repo_log_encoding) - self.user = repository.find_committer_user(self.committer) - end - - def scan_for_issues - scan_comment_for_issue_ids - end - - TIMELOG_RE = / - ( - ((\d+)(h|hours?))((\d+)(m|min)?)? - | - ((\d+)(h|hours?|m|min)) - | - (\d+):(\d+) - | - (\d+([\.,]\d+)?)h? - ) - /x - - def scan_comment_for_issue_ids - return if comments.blank? - # keywords used to reference issues - ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) - ref_keywords_any = ref_keywords.delete('*') - # keywords used to fix issues - fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) - - kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") - - referenced_issues = [] - - comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match| - action, refs = match[2], match[3] - next unless action.present? || ref_keywords_any - - refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m| - issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2] - if issue - referenced_issues << issue - fix_issue(issue) if fix_keywords.include?(action.to_s.downcase) - log_time(issue, hours) if hours && Setting.commit_logtime_enabled? - end - end - end - - referenced_issues.uniq! - self.issues = referenced_issues unless referenced_issues.empty? - end - - def short_comments - @short_comments || split_comments.first - end - - def long_comments - @long_comments || split_comments.last - end - - def text_tag(ref_project=nil) - tag = if scmid? - "commit:#{scmid}" - else - "r#{revision}" - end - if repository && repository.identifier.present? - tag = "#{repository.identifier}|#{tag}" - end - if ref_project && project && ref_project != project - tag = "#{project.identifier}:#{tag}" - end - tag - end - - # Returns the title used for the changeset in the activity/search results - def title - repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : '' - comm = short_comments.blank? ? '' : (': ' + short_comments) - "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}" - end - - # Returns the previous changeset - def previous - @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first - end - - # Returns the next changeset - def next - @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first - end - - # Creates a new Change from it's common parameters - def create_change(change) - Change.create(:changeset => self, - :action => change[:action], - :path => change[:path], - :from_path => change[:from_path], - :from_revision => change[:from_revision]) - end - - # Finds an issue that can be referenced by the commit message - def find_referenced_issue_by_id(id) - return nil if id.blank? - issue = Issue.find_by_id(id.to_i, :include => :project) - if Setting.commit_cross_project_ref? - # all issues can be referenced/fixed - elsif issue - # issue that belong to the repository project, a subproject or a parent project only - unless issue.project && - (project == issue.project || project.is_ancestor_of?(issue.project) || - project.is_descendant_of?(issue.project)) - issue = nil - end - end - issue - end - - private - - def fix_issue(issue) - status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) - if status.nil? - logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger - return issue - end - - # the issue may have been updated by the closure of another one (eg. duplicate) - issue.reload - # don't change the status is the issue is closed - return if issue.status && issue.status.is_closed? - - journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project))) - issue.status = status - unless Setting.commit_fix_done_ratio.blank? - issue.done_ratio = Setting.commit_fix_done_ratio.to_i - end - Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, - { :changeset => self, :issue => issue }) - unless issue.save - logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger - end - issue - end - - def log_time(issue, hours) - time_entry = TimeEntry.new( - :user => user, - :hours => hours, - :issue => issue, - :spent_on => commit_date, - :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project), - :locale => Setting.default_language) - ) - time_entry.activity = log_time_activity unless log_time_activity.nil? - - unless time_entry.save - logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger - end - time_entry - end - - def log_time_activity - if Setting.commit_logtime_activity_id.to_i > 0 - TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) - end - end - - def split_comments - comments =~ /\A(.+?)\r?\n(.*)$/m - @short_comments = $1 || comments - @long_comments = $2.to_s.strip - return @short_comments, @long_comments - end - - public - - # Strips and reencodes a commit log before insertion into the database - def self.normalize_comments(str, encoding) - Changeset.to_utf8(str.to_s.strip, encoding) - end - - def self.to_utf8(str, encoding) - Redmine::CodesetUtil.to_utf8(str, encoding) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/2069c7a6894bd57d0791bf6d175776952425d0ca.svn-base --- a/.svn/pristine/20/2069c7a6894bd57d0791bf6d175776952425d0ca.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,354 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'diff' - -# The WikiController follows the Rails REST controller pattern but with -# a few differences -# -# * index - shows a list of WikiPages grouped by page or date -# * new - not used -# * create - not used -# * show - will also show the form for creating a new wiki page -# * edit - used to edit an existing or new page -# * update - used to save a wiki page update to the database, including new pages -# * destroy - normal -# -# Other member and collection methods are also used -# -# TODO: still being worked on -class WikiController < ApplicationController - default_search_scope :wiki_pages - before_filter :find_wiki, :authorize - before_filter :find_existing_or_new_page, :only => [:show, :edit, :update] - before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] - accept_api_auth :index, :show, :update, :destroy - - helper :attachments - include AttachmentsHelper - helper :watchers - include Redmine::Export::PDF - - # List of pages, sorted alphabetically and by parent (hierarchy) - def index - load_pages_for_index - - respond_to do |format| - format.html { - @pages_by_parent_id = @pages.group_by(&:parent_id) - } - format.api - end - end - - # List of page, by last update - def date_index - load_pages_for_index - @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} - end - - # display a page (in editing mode if it doesn't exist) - def show - if @page.new_record? - if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request? - edit - render :action => 'edit' - else - render_404 - end - return - end - if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project) - deny_access - return - end - @content = @page.content_for_version(params[:version]) - if User.current.allowed_to?(:export_wiki_pages, @project) - if params[:format] == 'pdf' - send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf") - return - elsif params[:format] == 'html' - export = render_to_string :action => 'export', :layout => false - send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") - return - elsif params[:format] == 'txt' - send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") - return - end - end - @editable = editable? - @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) && - @content.current_version? && - Redmine::WikiFormatting.supports_section_edit? - - respond_to do |format| - format.html - format.api - end - end - - # edit an existing page or a new one - def edit - return render_403 unless editable? - if @page.new_record? - @page.content = WikiContent.new(:page => @page) - if params[:parent].present? - @page.parent = @page.wiki.find_page(params[:parent].to_s) - end - end - - @content = @page.content_for_version(params[:version]) - @content.text = initial_page_content(@page) if @content.text.blank? - # don't keep previous comment - @content.comments = nil - - # To prevent StaleObjectError exception when reverting to a previous version - @content.version = @page.content.version - - @text = @content.text - if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? - @section = params[:section].to_i - @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section) - render_404 if @text.blank? - end - end - - # Creates a new page or updates an existing one - def update - return render_403 unless editable? - was_new_page = @page.new_record? - @page.content = WikiContent.new(:page => @page) if @page.new_record? - @page.safe_attributes = params[:wiki_page] - - @content = @page.content - content_params = params[:content] - if content_params.nil? && params[:wiki_page].is_a?(Hash) - content_params = params[:wiki_page].slice(:text, :comments, :version) - end - content_params ||= {} - - @content.comments = content_params[:comments] - @text = content_params[:text] - if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? - @section = params[:section].to_i - @section_hash = params[:section_hash] - @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash) - else - @content.version = content_params[:version] if content_params[:version] - @content.text = @text - end - @content.author = User.current - - if @page.save_with_content - attachments = Attachment.attach_files(@page, params[:attachments]) - render_attachment_warning_if_needed(@page) - call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page}) - - respond_to do |format| - format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title } - format.api { - if was_new_page - render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title) - else - render_api_ok - end - } - end - else - respond_to do |format| - format.html { render :action => 'edit' } - format.api { render_validation_errors(@content) } - end - end - - rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError - # Optimistic locking exception - respond_to do |format| - format.html { - flash.now[:error] = l(:notice_locking_conflict) - render :action => 'edit' - } - format.api { render_api_head :conflict } - end - rescue ActiveRecord::RecordNotSaved - respond_to do |format| - format.html { render :action => 'edit' } - format.api { render_validation_errors(@content) } - end - end - - # rename a page - def rename - return render_403 unless editable? - @page.redirect_existing_links = true - # used to display the *original* title if some AR validation errors occur - @original_title = @page.pretty_title - if request.post? && @page.update_attributes(params[:wiki_page]) - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'show', :project_id => @project, :id => @page.title - end - end - - def protect - @page.update_attribute :protected, params[:protected] - redirect_to :action => 'show', :project_id => @project, :id => @page.title - end - - # show page history - def history - @version_count = @page.content.versions.count - @version_pages = Paginator.new self, @version_count, per_page_option, params['page'] - # don't load text - @versions = @page.content.versions.find :all, - :select => "id, author_id, comments, updated_on, version", - :order => 'version DESC', - :limit => @version_pages.items_per_page + 1, - :offset => @version_pages.current.offset - - render :layout => false if request.xhr? - end - - def diff - @diff = @page.diff(params[:version], params[:version_from]) - render_404 unless @diff - end - - def annotate - @annotate = @page.annotate(params[:version]) - render_404 unless @annotate - end - - # Removes a wiki page and its history - # Children can be either set as root pages, removed or reassigned to another parent page - def destroy - return render_403 unless editable? - - @descendants_count = @page.descendants.size - if @descendants_count > 0 - case params[:todo] - when 'nullify' - # Nothing to do - when 'destroy' - # Removes all its descendants - @page.descendants.each(&:destroy) - when 'reassign' - # Reassign children to another parent page - reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i) - return unless reassign_to - @page.children.each do |child| - child.update_attribute(:parent, reassign_to) - end - else - @reassignable_to = @wiki.pages - @page.self_and_descendants - # display the destroy form if it's a user request - return unless api_request? - end - end - @page.destroy - respond_to do |format| - format.html { redirect_to :action => 'index', :project_id => @project } - format.api { render_api_ok } - end - end - - def destroy_version - return render_403 unless editable? - - @content = @page.content_for_version(params[:version]) - @content.destroy - redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project - end - - # Export wiki to a single pdf or html file - def export - @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}]) - respond_to do |format| - format.html { - export = render_to_string :action => 'export_multiple', :layout => false - send_data(export, :type => 'text/html', :filename => "wiki.html") - } - format.pdf { - send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf") - } - end - end - - def preview - page = @wiki.find_page(params[:id]) - # page is nil when previewing a new page - return render_403 unless page.nil? || editable?(page) - if page - @attachements = page.attachments - @previewed = page.content - end - @text = params[:content][:text] - render :partial => 'common/preview' - end - - def add_attachment - return render_403 unless editable? - attachments = Attachment.attach_files(@page, params[:attachments]) - render_attachment_warning_if_needed(@page) - redirect_to :action => 'show', :id => @page.title, :project_id => @project - end - -private - - def find_wiki - @project = Project.find(params[:project_id]) - @wiki = @project.wiki - render_404 unless @wiki - rescue ActiveRecord::RecordNotFound - render_404 - end - - # Finds the requested page or a new page if it doesn't exist - def find_existing_or_new_page - @page = @wiki.find_or_new_page(params[:id]) - if @wiki.page_found_with_redirect? - redirect_to params.update(:id => @page.title) - end - end - - # Finds the requested page and returns a 404 error if it doesn't exist - def find_existing_page - @page = @wiki.find_page(params[:id]) - if @page.nil? - render_404 - return - end - if @wiki.page_found_with_redirect? - redirect_to params.update(:id => @page.title) - end - end - - # Returns true if the current user is allowed to edit the page, otherwise false - def editable?(page = @page) - page.editable_by?(User.current) - end - - # Returns the default content of a new wiki page - def initial_page_content(page) - helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) - extend helper unless self.instance_of?(helper) - helper.instance_method(:initial_page_content).bind(self).call(page) - end - - def load_pages_for_index - @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/209f9ebc3aa58812be26dbeb5ba5ab0a2d6a75b3.svn-base --- a/.svn/pristine/20/209f9ebc3aa58812be26dbeb5ba5ab0a2d6a75b3.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,242 +0,0 @@ -# encoding: utf-8 -# -# Helpers to sort tables using clickable column headers. -# -# Author: Stuart Rackham , March 2005. -# Jean-Philippe Lang, 2009 -# License: This source code is released under the MIT license. -# -# - Consecutive clicks toggle the column's sort order. -# - Sort state is maintained by a session hash entry. -# - CSS classes identify sort column and state. -# - Typically used in conjunction with the Pagination module. -# -# Example code snippets: -# -# Controller: -# -# helper :sort -# include SortHelper -# -# def list -# sort_init 'last_name' -# sort_update %w(first_name last_name) -# @items = Contact.find_all nil, sort_clause -# end -# -# Controller (using Pagination module): -# -# helper :sort -# include SortHelper -# -# def list -# sort_init 'last_name' -# sort_update %w(first_name last_name) -# @contact_pages, @items = paginate :contacts, -# :order_by => sort_clause, -# :per_page => 10 -# end -# -# View (table header in list.rhtml): -# -# -# -# <%= sort_header_tag('id', :title => 'Sort by contact ID') %> -# <%= sort_header_tag('last_name', :caption => 'Name') %> -# <%= sort_header_tag('phone') %> -# <%= sort_header_tag('address', :width => 200) %> -# -# -# -# - Introduces instance variables: @sort_default, @sort_criteria -# - Introduces param :sort -# - -module SortHelper - class SortCriteria - - def initialize - @criteria = [] - end - - def available_criteria=(criteria) - unless criteria.is_a?(Hash) - criteria = criteria.inject({}) {|h,k| h[k] = k; h} - end - @available_criteria = criteria - end - - def from_param(param) - @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]} - normalize! - end - - def criteria=(arg) - @criteria = arg - normalize! - end - - def to_param - @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',') - end - - def to_sql - sql = @criteria.collect do |k,o| - if s = @available_criteria[k] - (o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ') - end - end.compact.join(', ') - sql.blank? ? nil : sql - end - - def to_a - @criteria.dup - end - - def add!(key, asc) - @criteria.delete_if {|k,o| k == key} - @criteria = [[key, asc]] + @criteria - normalize! - end - - def add(*args) - r = self.class.new.from_param(to_param) - r.add!(*args) - r - end - - def first_key - @criteria.first && @criteria.first.first - end - - def first_asc? - @criteria.first && @criteria.first.last - end - - def empty? - @criteria.empty? - end - - private - - def normalize! - @criteria ||= [] - @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]} - @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria - @criteria.slice!(3) - self - end - - # Appends DESC to the sort criterion unless it has a fixed order - def append_desc(criterion) - if criterion =~ / (asc|desc)$/i - criterion - else - "#{criterion} DESC" - end - end - end - - def sort_name - controller_name + '_' + action_name + '_sort' - end - - # Initializes the default sort. - # Examples: - # - # sort_init 'name' - # sort_init 'id', 'desc' - # sort_init ['name', ['id', 'desc']] - # sort_init [['name', 'desc'], ['id', 'desc']] - # - def sort_init(*args) - case args.size - when 1 - @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]] - when 2 - @sort_default = [[args.first, args.last]] - else - raise ArgumentError - end - end - - # Updates the sort state. Call this in the controller prior to calling - # sort_clause. - # - criteria can be either an array or a hash of allowed keys - # - def sort_update(criteria, sort_name=nil) - sort_name ||= self.sort_name - @sort_criteria = SortCriteria.new - @sort_criteria.available_criteria = criteria - @sort_criteria.from_param(params[:sort] || session[sort_name]) - @sort_criteria.criteria = @sort_default if @sort_criteria.empty? - session[sort_name] = @sort_criteria.to_param - end - - # Clears the sort criteria session data - # - def sort_clear - session[sort_name] = nil - end - - # Returns an SQL sort clause corresponding to the current sort state. - # Use this to sort the controller's table items collection. - # - def sort_clause() - @sort_criteria.to_sql - end - - def sort_criteria - @sort_criteria - end - - # Returns a link which sorts by the named column. - # - # - column is the name of an attribute in the sorted record collection. - # - the optional caption explicitly specifies the displayed link text. - # - 2 CSS classes reflect the state of the link: sort and asc or desc - # - def sort_link(column, caption, default_order) - css, order = nil, default_order - - if column.to_s == @sort_criteria.first_key - if @sort_criteria.first_asc? - css = 'sort asc' - order = 'desc' - else - css = 'sort desc' - order = 'asc' - end - end - caption = column.to_s.humanize unless caption - - sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } - url_options = params.merge(sort_options) - - # Add project_id to url_options - url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) - - link_to_content_update(h(caption), url_options, :class => css) - end - - # Returns a table header tag with a sort link for the named column - # attribute. - # - # Options: - # :caption The displayed link name (defaults to titleized column name). - # :title The tag's 'title' attribute (defaults to 'Sort by :caption'). - # - # Other options hash entries generate additional table header tag attributes. - # - # Example: - # - # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %> - # - def sort_header_tag(column, options = {}) - caption = options.delete(:caption) || column.to_s.humanize - default_order = options.delete(:default_order) || 'asc' - options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title] - content_tag('th', sort_link(column, caption, default_order), options) - end -end - diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/20a3a3fb3196d2079735a43085a70288c004c4e8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/20a3a3fb3196d2079735a43085a70288c004c4e8.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,17 @@ + + + + + Redmine 404 error + + + +

    Page not found

    +

    The page you were trying to access doesn't exist or has been removed.

    +

    Back

    + + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/20cf4d4753525274da375856e4def6acfb536bf9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/20cf4d4753525274da375856e4def6acfb536bf9.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +# Redmine - project management software +# Copyright (C) 2006-2014 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 TimeEntryReportsControllerTest < ActionController::TestCase + tests TimelogController + + fixtures :projects, :enabled_modules, :roles, :members, :member_roles, + :issues, :time_entries, :users, :trackers, :enumerations, + :issue_statuses, :custom_fields, :custom_values + + include Redmine::I18n + + def setup + Setting.default_language = "en" + end + + def test_report_at_project_level + get :report, :project_id => 'ecookbook' + assert_response :success + assert_template 'report' + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries/report", :id => 'query_form'} + end + + def test_report_all_projects + get :report + assert_response :success + assert_template 'report' + assert_tag :form, + :attributes => {:action => "/time_entries/report", :id => 'query_form'} + end + + def test_report_all_projects_denied + r = Role.anonymous + r.permissions.delete(:view_time_entries) + r.permissions_will_change! + r.save + get :report + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport' + end + + def test_report_all_projects_one_criteria + get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "8.65", "%.2f" % assigns(:report).total_hours + end + + def test_report_all_time + get :report, :project_id => 1, :criteria => ['project', 'issue'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + end + + def test_report_all_time_by_day + get :report, :project_id => 1, :criteria => ['project', 'issue'], :columns => 'day' + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + assert_tag :tag => 'th', :content => '2007-03-12' + end + + def test_report_one_criteria + get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criteria => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "8.65", "%.2f" % assigns(:report).total_hours + end + + def test_report_two_criteria + get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + end + + def test_report_custom_field_criteria_with_multiple_values + field = TimeEntryCustomField.create!(:name => 'multi', :field_format => 'list', :possible_values => ['value1', 'value2']) + entry = TimeEntry.create!(:project => Project.find(1), :hours => 1, :activity_id => 10, :user => User.find(2), :spent_on => Date.today) + CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value1') + CustomValue.create!(:customized => entry, :custom_field => field, :value => 'value2') + + get :report, :project_id => 1, :columns => 'day', :criteria => ["cf_#{field.id}"] + assert_response :success + end + + def test_report_one_day + get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["user", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "4.25", "%.2f" % assigns(:report).total_hours + end + + def test_report_at_issue_level + get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["user", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "154.25", "%.2f" % assigns(:report).total_hours + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'} + end + + def test_report_by_week_should_use_commercial_year + TimeEntry.delete_all + TimeEntry.generate!(:hours => '2', :spent_on => '2009-12-25') # 2009-52 + TimeEntry.generate!(:hours => '4', :spent_on => '2009-12-31') # 2009-53 + TimeEntry.generate!(:hours => '8', :spent_on => '2010-01-01') # 2009-53 + TimeEntry.generate!(:hours => '16', :spent_on => '2010-01-05') # 2010-1 + + get :report, :columns => 'week', :from => "2009-12-25", :to => "2010-01-05", :criteria => ["project"] + assert_response :success + + assert_select '#time-report thead tr' do + assert_select 'th:nth-child(1)', :text => 'Project' + assert_select 'th:nth-child(2)', :text => '2009-52' + assert_select 'th:nth-child(3)', :text => '2009-53' + assert_select 'th:nth-child(4)', :text => '2010-1' + assert_select 'th:nth-child(5)', :text => 'Total time' + end + assert_select '#time-report tbody tr' do + assert_select 'td:nth-child(1)', :text => 'eCookbook' + assert_select 'td:nth-child(2)', :text => '2.00' + assert_select 'td:nth-child(3)', :text => '12.00' + assert_select 'td:nth-child(4)', :text => '16.00' + assert_select 'td:nth-child(5)', :text => '30.00' # Total + end + end + + def test_report_should_propose_association_custom_fields + get :report + assert_response :success + assert_template 'report' + + assert_select 'select[name=?]', 'criteria[]' do + assert_select 'option[value=cf_1]', {:text => 'Database'}, 'Issue custom field not found' + assert_select 'option[value=cf_3]', {:text => 'Development status'}, 'Project custom field not found' + assert_select 'option[value=cf_7]', {:text => 'Billable'}, 'TimeEntryActivity custom field not found' + end + end + + def test_report_with_association_custom_fields + get :report, :criteria => ['cf_1', 'cf_3', 'cf_7'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal 3, assigns(:report).criteria.size + assert_equal "162.90", "%.2f" % assigns(:report).total_hours + + # Custom fields columns + assert_select 'th', :text => 'Database' + assert_select 'th', :text => 'Development status' + assert_select 'th', :text => 'Billable' + + # Custom field row + assert_select 'tr' do + assert_select 'td', :text => 'MySQL' + assert_select 'td.hours', :text => '1.00' + end + end + + def test_report_one_criteria_no_result + get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criteria => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:report) + assert_equal "0.00", "%.2f" % assigns(:report).total_hours + end + + def test_report_status_criterion + get :report, :project_id => 1, :criteria => ['status'] + assert_response :success + assert_template 'report' + assert_tag :tag => 'th', :content => 'Status' + assert_tag :tag => 'td', :content => 'New' + end + + def test_report_all_projects_csv_export + get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", + :criteria => ["project", "user", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first + # Total row + assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last + end + + def test_report_csv_export + get :report, :project_id => 1, :columns => 'month', + :from => "2007-01-01", :to => "2007-06-30", + :criteria => ["project", "user", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first + # Total row + assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last + end + + def test_csv_big_5 + Setting.default_language = "zh-TW" + str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88" + str_big5 = "\xa4@\xa4\xeb" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + str_big5.force_encoding('Big5') + end + user = User.find_by_id(3) + user.firstname = str_utf8 + user.lastname = "test-lastname" + assert user.save + comments = "test_csv_big_5" + te1 = TimeEntry.create(:spent_on => '2011-11-11', + :hours => 7.3, + :project => Project.find(1), + :user => user, + :activity => TimeEntryActivity.find_by_name('Design'), + :comments => comments) + + te2 = TimeEntry.find_by_comments(comments) + assert_not_nil te2 + assert_equal 7.3, te2.hours + assert_equal 3, te2.user_id + + get :report, :project_id => 1, :columns => 'day', + :from => "2011-11-11", :to => "2011-11-11", + :criteria => ["user"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp" + s2 = "\xa4u\xae\xc9\xc1`\xadp" + if s1.respond_to?(:force_encoding) + s1.force_encoding('Big5') + s2.force_encoding('Big5') + end + assert_equal s1, lines.first + # Total row + assert_equal "#{str_big5} #{user.lastname},7.30,7.30", lines[1] + assert_equal "#{s2},7.30,7.30", lines[2] + + str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)" + if str_tw.respond_to?(:force_encoding) + str_tw.force_encoding('UTF-8') + end + assert_equal str_tw, l(:general_lang_name) + assert_equal 'Big5', l(:general_csv_encoding) + assert_equal ',', l(:general_csv_separator) + assert_equal '.', l(:general_csv_decimal_separator) + end + + def test_csv_cannot_convert_should_be_replaced_big_5 + Setting.default_language = "zh-TW" + str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + end + user = User.find_by_id(3) + user.firstname = str_utf8 + user.lastname = "test-lastname" + assert user.save + comments = "test_replaced" + te1 = TimeEntry.create(:spent_on => '2011-11-11', + :hours => 7.3, + :project => Project.find(1), + :user => user, + :activity => TimeEntryActivity.find_by_name('Design'), + :comments => comments) + + te2 = TimeEntry.find_by_comments(comments) + assert_not_nil te2 + assert_equal 7.3, te2.hours + assert_equal 3, te2.user_id + + get :report, :project_id => 1, :columns => 'day', + :from => "2011-11-11", :to => "2011-11-11", + :criteria => ["user"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp" + if s1.respond_to?(:force_encoding) + s1.force_encoding('Big5') + end + assert_equal s1, lines.first + # Total row + s2 = "" + if s2.respond_to?(:force_encoding) + s2 = "\xa5H?" + s2.force_encoding('Big5') + elsif RUBY_PLATFORM == 'java' + s2 = "??" + else + s2 = "\xa5H???" + end + assert_equal "#{s2} #{user.lastname},7.30,7.30", lines[1] + end + + def test_csv_fr + with_settings :default_language => "fr" do + str1 = "test_csv_fr" + user = User.find_by_id(3) + te1 = TimeEntry.create(:spent_on => '2011-11-11', + :hours => 7.3, + :project => Project.find(1), + :user => user, + :activity => TimeEntryActivity.find_by_name('Design'), + :comments => str1) + + te2 = TimeEntry.find_by_comments(str1) + assert_not_nil te2 + assert_equal 7.3, te2.hours + assert_equal 3, te2.user_id + + get :report, :project_id => 1, :columns => 'day', + :from => "2011-11-11", :to => "2011-11-11", + :criteria => ["user"], :format => "csv" + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + s1 = "Utilisateur;2011-11-11;Temps total" + s2 = "Temps total" + if s1.respond_to?(:force_encoding) + s1.force_encoding('ISO-8859-1') + s2.force_encoding('ISO-8859-1') + end + assert_equal s1, lines.first + # Total row + assert_equal "#{user.firstname} #{user.lastname};7,30;7,30", lines[1] + assert_equal "#{s2};7,30;7,30", lines[2] + + str_fr = "Fran\xc3\xa7ais" + if str_fr.respond_to?(:force_encoding) + str_fr.force_encoding('UTF-8') + end + assert_equal str_fr, l(:general_lang_name) + assert_equal 'ISO-8859-1', l(:general_csv_encoding) + assert_equal ';', l(:general_csv_separator) + assert_equal ',', l(:general_csv_decimal_separator) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/20cfc1def6cf64da0d22e9f4697140fef8698ede.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/20cfc1def6cf64da0d22e9f4697140fef8698ede.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,24 @@ +class AddUniqueIndexOnCustomFieldsTrackers < ActiveRecord::Migration + def up + table_name = "#{CustomField.table_name_prefix}custom_fields_trackers#{CustomField.table_name_suffix}" + duplicates = CustomField.connection.select_rows("SELECT custom_field_id, tracker_id FROM #{table_name} GROUP BY custom_field_id, tracker_id HAVING COUNT(*) > 1") + duplicates.each do |custom_field_id, tracker_id| + # Removes duplicate rows + CustomField.connection.execute("DELETE FROM #{table_name} WHERE custom_field_id=#{custom_field_id} AND tracker_id=#{tracker_id}") + # And insert one + CustomField.connection.execute("INSERT INTO #{table_name} (custom_field_id, tracker_id) VALUES (#{custom_field_id}, #{tracker_id})") + end + + if index_exists? :custom_fields_trackers, [:custom_field_id, :tracker_id] + remove_index :custom_fields_trackers, [:custom_field_id, :tracker_id] + end + add_index :custom_fields_trackers, [:custom_field_id, :tracker_id], :unique => true + end + + def down + if index_exists? :custom_fields_trackers, [:custom_field_id, :tracker_id] + remove_index :custom_fields_trackers, [:custom_field_id, :tracker_id] + end + add_index :custom_fields_trackers, [:custom_field_id, :tracker_id] + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/20/20f8dff24ed27f7604eb718009f2a8bf899ab23c.svn-base --- a/.svn/pristine/20/20f8dff24ed27f7604eb718009f2a8bf899ab23c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'pp' -class ApiTest::NewsTest < ActionController::IntegrationTest - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows, - :news - - def setup - Setting.rest_api_enabled = '1' - end - - context "GET /news" do - context ".xml" do - should "return news" do - get '/news.xml' - - assert_tag :tag => 'news', - :attributes => {:type => 'array'}, - :child => { - :tag => 'news', - :child => { - :tag => 'id', - :content => '2' - } - } - end - end - - context ".json" do - should "return news" do - get '/news.json' - - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Array, json['news'] - assert_kind_of Hash, json['news'].first - assert_equal 2, json['news'].first['id'] - end - end - end - - context "GET /projects/:project_id/news" do - context ".xml" do - should_allow_api_authentication(:get, "/projects/onlinestore/news.xml") - - should "return news" do - get '/projects/ecookbook/news.xml' - - assert_tag :tag => 'news', - :attributes => {:type => 'array'}, - :child => { - :tag => 'news', - :child => { - :tag => 'id', - :content => '2' - } - } - end - end - - context ".json" do - should_allow_api_authentication(:get, "/projects/onlinestore/news.json") - - should "return news" do - get '/projects/ecookbook/news.json' - - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Array, json['news'] - assert_kind_of Hash, json['news'].first - assert_equal 2, json['news'].first['id'] - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/21/212b78099cd591bc9596c563f09ce2ddb8eb84ad.svn-base --- a/.svn/pristine/21/212b78099cd591bc9596c563f09ce2ddb8eb84ad.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingMembersTest < ActionController::IntegrationTest - def test_members - assert_routing( - { :method => 'get', :path => "/projects/5234/memberships.xml" }, - { :controller => 'members', :action => 'index', :project_id => '5234', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/memberships/5234.xml" }, - { :controller => 'members', :action => 'show', :id => '5234', :format => 'xml' } - ) - assert_routing( - { :method => 'post', :path => "/projects/5234/memberships" }, - { :controller => 'members', :action => 'create', :project_id => '5234' } - ) - assert_routing( - { :method => 'post', :path => "/projects/5234/memberships.xml" }, - { :controller => 'members', :action => 'create', :project_id => '5234', :format => 'xml' } - ) - assert_routing( - { :method => 'put', :path => "/memberships/5234" }, - { :controller => 'members', :action => 'update', :id => '5234' } - ) - assert_routing( - { :method => 'put', :path => "/memberships/5234.xml" }, - { :controller => 'members', :action => 'update', :id => '5234', :format => 'xml' } - ) - assert_routing( - { :method => 'delete', :path => "/memberships/5234" }, - { :controller => 'members', :action => 'destroy', :id => '5234' } - ) - assert_routing( - { :method => 'delete', :path => "/memberships/5234.xml" }, - { :controller => 'members', :action => 'destroy', :id => '5234', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/projects/5234/memberships/autocomplete" }, - { :controller => 'members', :action => 'autocomplete', :project_id => '5234' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/21/2133044f3bd39e97fd612d3eaa27ffd002d52b29.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/2133044f3bd39e97fd612d3eaa27ffd002d52b29.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,46 @@ +<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %> + +
    + <%= render :partial => 'navigation' %> +
    + +

    <%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %>

    + +<%= render :partial => 'link_to_functions' %> + +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + +
    + + + <% line_num = 1; previous_revision = nil %> + <% syntax_highlight_lines(@path, Redmine::CodesetUtil.to_utf8_by_setting(@annotate.content)).each do |line| %> + <% revision = @annotate.revisions[line_num - 1] %> + + + + + + + <% line_num += 1; previous_revision = revision %> + <% end %> + +
    <%= line_num %> + <% if revision && revision != previous_revision %> + <%= revision.identifier ? + link_to_revision(revision, @repository) : format_revision(revision) %> + <% end %> + + <% if revision && revision != previous_revision %> + <% author = Redmine::CodesetUtil.to_utf8(revision.author.to_s, + @repository.repo_log_encoding) %> + <%= author.split('<').first %> + <% end %> +
    <%= line.html_safe %>
    +
    + +<% html_title(l(:button_annotate)) -%> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/21/213447b90bcf49e61197299b7ccf1319e4dafbe5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/213447b90bcf49e61197299b7ccf1319e4dafbe5.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,19 @@ +<%= title [l(:label_custom_field_plural), custom_fields_path], + [l(@custom_field.type_name), custom_fields_path(:tab => @custom_field.class.name)], + l(:label_custom_field_new) %> + +<%= labelled_form_for :custom_field, @custom_field, :url => custom_fields_path, :html => {:id => 'custom_field_form'} do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= hidden_field_tag 'type', @custom_field.type %> +<% end %> + +<%= javascript_tag do %> +$('#custom_field_field_format').change(function(){ + $.ajax({ + url: '<%= new_custom_field_path(:format => 'js') %>', + type: 'get', + data: $('#custom_field_form').serialize(), + complete: toggleDisabledInit + }); +}); +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/21/213cd47b5ee47b691425e1fcb6dffb207c707d8c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/213cd47b5ee47b691425e1fcb6dffb207c707d8c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.9.2 - 2012-12-26 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2C%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=759fcf&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=628db6&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=628db6&iconColorDefault=759fcf&bgColorHover=eef5fd&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=628db6&fcHover=628db6&iconColorHover=759fcf&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=628db6&fcActive=628db6&iconColorActive=759fcf&bgColorHighlight=759fcf&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=628db6&fcHighlight=363636&iconColorHighlight=759fcf&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright (c) 2012 jQuery Foundation and other contributors Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{zoom:1}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:absolute;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin-top:2px;padding:.5em .5em .5em .7em;zoom:1}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-noicons{padding-left:.7em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto;zoom:1}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}* html .ui-autocomplete{width:1px}.ui-button{display:inline-block;position:relative;padding:0;margin-right:.1em;cursor:pointer;text-align:center;zoom:1;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:1.4}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month-year{width:100%}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0em}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current{float:right}.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker-cover{position:absolute;z-index:-1;filter:mask();top:-4px;left:-4px;width:200px;height:200px}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;width:300px;overflow:hidden}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 16px .1em 0}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:19px;margin:-10px 0 0 0;padding:1px;height:18px}.ui-dialog .ui-dialog-titlebar-close span{display:block;margin:1px}.ui-dialog .ui-dialog-titlebar-close:hover,.ui-dialog .ui-dialog-titlebar-close:focus{padding:0}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto;zoom:1}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin:.5em 0 0 0;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:14px;height:14px;right:3px;bottom:3px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;zoom:1;width:100%}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;zoom:1;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em;zoom:1}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav li a{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active a,.ui-tabs .ui-tabs-nav li.ui-state-disabled a,.ui-tabs .ui-tabs-nav li.ui-tabs-loading a{cursor:text}.ui-tabs .ui-tabs-nav li a,.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}* html .ui-tooltip{background-image:none}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #628db6;background:#759fcf url(images/ui-bg_gloss-wave_35_759fcf_500x100.png) 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#628db6}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#628db6;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #628db6;background:#eef5fd url(images/ui-bg_glass_100_eef5fd_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#628db6}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#628db6;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #628db6;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#628db6}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#628db6;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #628db6;background:#759fcf url(images/ui-bg_highlight-soft_75_759fcf_1x100.png) 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px;background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_ffffff_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_759fcf_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_759fcf_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_759fcf_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_759fcf_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_ffd27a_256x240.png)}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;-khtml-border-top-left-radius:4px;border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;-khtml-border-top-right-radius:4px;border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;-khtml-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;-khtml-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);-moz-border-radius:5px;-khtml-border-radius:5px;-webkit-border-radius:5px;border-radius:5px} diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/21/219b4c528b9f8153a3c5fd8eb58734b3b63b1db2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/219b4c528b9f8153a3c5fd8eb58734b3b63b1db2.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1138 @@ +# Polish translations for Ruby on Rails +# by Jacek Becela (jacek.becela@gmail.com, http://github.com/ncr) +# by Krzysztof Podejma (kpodejma@customprojects.pl, http://www.customprojects.pl) + +pl: + number: + format: + separator: "," + delimiter: " " + precision: 2 + currency: + format: + format: "%n %u" + unit: "PLN" + percentage: + format: + delimiter: "" + precision: + format: + delimiter: "" + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "B" + other: "B" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + direction: ltr + date: + formats: + default: "%Y-%m-%d" + short: "%d %b" + long: "%d %B %Y" + + day_names: [Niedziela, PoniedziaÅ‚ek, Wtorek, Åšroda, Czwartek, PiÄ…tek, Sobota] + abbr_day_names: [nie, pon, wto, Å›ro, czw, pia, sob] + + month_names: [~, StyczeÅ„, Luty, Marzec, KwiecieÅ„, Maj, Czerwiec, Lipiec, SierpieÅ„, WrzesieÅ„, Październik, Listopad, GrudzieÅ„] + abbr_month_names: [~, sty, lut, mar, kwi, maj, cze, lip, sie, wrz, paź, lis, gru] + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y, %H:%M:%S %z" + time: "%H:%M" + short: "%d %b, %H:%M" + long: "%d %B %Y, %H:%M" + am: "przed poÅ‚udniem" + pm: "po poÅ‚udniu" + + datetime: + distance_in_words: + half_a_minute: "pół minuty" + less_than_x_seconds: + one: "mniej niż sekundÄ™" + few: "mniej niż %{count} sekundy" + other: "mniej niż %{count} sekund" + x_seconds: + one: "sekundÄ™" + few: "%{count} sekundy" + other: "%{count} sekund" + less_than_x_minutes: + one: "mniej niż minutÄ™" + few: "mniej niż %{count} minuty" + other: "mniej niż %{count} minut" + x_minutes: + one: "minutÄ™" + few: "%{count} minuty" + other: "%{count} minut" + about_x_hours: + one: "okoÅ‚o godziny" + other: "okoÅ‚o %{count} godzin" + x_hours: + one: "1 godzina" + other: "%{count} godzin" + x_days: + one: "1 dzieÅ„" + other: "%{count} dni" + about_x_months: + one: "okoÅ‚o miesiÄ…ca" + other: "okoÅ‚o %{count} miesiÄ™cy" + x_months: + one: "1 miesiÄ…c" + few: "%{count} miesiÄ…ce" + other: "%{count} miesiÄ™cy" + about_x_years: + one: "okoÅ‚o roku" + other: "okoÅ‚o %{count} lat" + over_x_years: + one: "ponad rok" + few: "ponad %{count} lata" + other: "ponad %{count} lat" + almost_x_years: + one: "prawie 1 rok" + few: "prawie %{count} lata" + other: "prawie %{count} lat" + + activerecord: + errors: + template: + header: + one: "%{model} nie zostaÅ‚ zachowany z powodu jednego błędu" + other: "%{model} nie zostaÅ‚ zachowany z powodu %{count} błędów" + body: "Błędy dotyczÄ… nastÄ™pujÄ…cych pól:" + messages: + inclusion: "nie znajduje siÄ™ na liÅ›cie dopuszczalnych wartoÅ›ci" + exclusion: "znajduje siÄ™ na liÅ›cie zabronionych wartoÅ›ci" + invalid: "jest nieprawidÅ‚owe" + confirmation: "nie zgadza siÄ™ z potwierdzeniem" + accepted: "musi być zaakceptowane" + empty: "nie może być puste" + blank: "nie może być puste" + too_long: "jest za dÅ‚ugie (maksymalnie %{count} znaków)" + too_short: "jest za krótkie (minimalnie %{count} znaków)" + wrong_length: "jest nieprawidÅ‚owej dÅ‚ugoÅ›ci (powinna wynosić %{count} znaków)" + taken: "jest już zajÄ™te" + not_a_number: "nie jest liczbÄ…" + greater_than: "musi być wiÄ™ksze niż %{count}" + greater_than_or_equal_to: "musi być wiÄ™ksze lub równe %{count}" + equal_to: "musi być równe %{count}" + less_than: "musi być mniejsze niż %{count}" + less_than_or_equal_to: "musi być mniejsze lub równe %{count}" + odd: "musi być nieparzyste" + even: "musi być parzyste" + greater_than_start_date: "musi być wiÄ™ksze niż poczÄ…tkowa data" + not_same_project: "nie należy do tego samego projektu" + circular_dependency: "Ta relacja może wytworzyć zapÄ™tlonÄ… zależność" + cant_link_an_issue_with_a_descendant: "Zagadnienie nie może zostać powiÄ…zane z jednym z wÅ‚asnych podzagadnieÅ„" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + support: + array: + sentence_connector: "i" + skip_last_comma: true + + # Keep this line in order to avoid problems with Windows Notepad UTF-8 EF-BB-BFidea... + # Best regards from Lublin@Poland :-) + # PL translation by Mariusz@Olejnik.net, + # Wiktor Wandachowicz , 2010 + + actionview_instancetag_blank_option: ProszÄ™ wybrać + actionview_instancetag_blank_option: wybierz + + button_activate: Aktywuj + button_add: Dodaj + button_annotate: Adnotuj + button_apply: Ustaw + button_archive: Archiwizuj + button_back: Wstecz + button_cancel: Anuluj + button_change: ZmieÅ„ + button_change_password: ZmieÅ„ hasÅ‚o + button_check_all: Zaznacz wszystko + button_clear: Wyczyść + button_configure: Konfiguruj + button_copy: Kopia + button_create: Stwórz + button_delete: UsuÅ„ + button_download: Pobierz + button_edit: Edytuj + button_list: Lista + button_lock: Zablokuj + button_log_time: Dziennik + button_login: Login + button_move: PrzenieÅ› + button_quote: Cytuj + button_rename: ZmieÅ„ nazwÄ™ + button_reply: Odpowiedz + button_reset: Resetuj + button_rollback: Przywróć do tej wersji + button_save: Zapisz + button_sort: Sortuj + button_submit: WyÅ›lij + button_test: Testuj + button_unarchive: Przywróć z archiwum + button_uncheck_all: Odznacz wszystko + button_unlock: Odblokuj + button_unwatch: Nie obserwuj + button_update: Uaktualnij + button_view: Pokaż + button_watch: Obserwuj + default_activity_design: Projektowanie + default_activity_development: Rozwój + default_doc_category_tech: Dokumentacja techniczna + default_doc_category_user: Dokumentacja użytkownika + default_issue_status_in_progress: W toku + default_issue_status_closed: ZamkniÄ™ty + default_issue_status_feedback: Odpowiedź + default_issue_status_new: Nowy + default_issue_status_rejected: Odrzucony + default_issue_status_resolved: RozwiÄ…zany + default_priority_high: Wysoki + default_priority_immediate: Natychmiastowy + default_priority_low: Niski + default_priority_normal: Normalny + default_priority_urgent: Pilny + default_role_developer: Programista + default_role_manager: Kierownik + default_role_reporter: ZgÅ‚aszajÄ…cy + default_tracker_bug: Błąd + default_tracker_feature: Zadanie + default_tracker_support: Wsparcie + enumeration_activities: DziaÅ‚ania (Å›ledzenie czasu) + enumeration_doc_categories: Kategorie dokumentów + enumeration_issue_priorities: Priorytety zagadnieÅ„ + error_can_t_load_default_data: "DomyÅ›lna konfiguracja nie może być zaÅ‚adowana: %{value}" + error_issue_not_found_in_project: 'Zagadnienie nie zostaÅ‚o znalezione lub nie należy do tego projektu' + error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji." + error_scm_command_failed: "WystÄ…piÅ‚ błąd przy próbie dostÄ™pu do repozytorium: %{value}" + error_scm_not_found: "Obiekt lub wersja nie zostaÅ‚y znalezione w repozytorium." + field_account: Konto + field_activity: Aktywność + field_admin: Administrator + field_assignable: Zagadnienia mogÄ… być przypisane do tej roli + field_assigned_to: Przypisany do + field_attr_firstname: ImiÄ™ atrybut + field_attr_lastname: Nazwisko atrybut + field_attr_login: Login atrybut + field_attr_mail: E-mail atrybut + field_auth_source: Tryb uwierzytelniania + field_author: Autor + field_base_dn: Base DN + field_category: Kategoria + field_column_names: Nazwy kolumn + field_comments: Komentarz + field_comments_sorting: Pokazuj komentarze + field_created_on: Data utworzenia + field_default_value: DomyÅ›lny + field_delay: Opóźnienie + field_description: Opis + field_done_ratio: "% Wykonania" + field_downloads: PobraÅ„ + field_due_date: Data oddania + field_effective_date: Data + field_estimated_hours: Szacowany czas + field_field_format: Format + field_filename: Plik + field_filesize: Rozmiar + field_firstname: ImiÄ™ + field_fixed_version: Wersja docelowa + field_hide_mail: Ukryj mój adres e-mail + field_homepage: Strona www + field_host: Host + field_hours: Godzin + field_identifier: Identyfikator + field_is_closed: Zagadnienie zamkniÄ™te + field_is_default: DomyÅ›lny status + field_is_filter: Atrybut filtrowania + field_is_for_all: Dla wszystkich projektów + field_is_in_roadmap: Zagadnienie pokazywane na mapie + field_is_public: Publiczny + field_is_required: Wymagane + field_issue: Zagadnienie + field_issue_to: PowiÄ…zania zagadnienia + field_language: JÄ™zyk + field_last_login_on: Ostatnie połączenie + field_lastname: Nazwisko + field_login: Login + field_mail: E-mail + field_mail_notification: Powiadomienia e-mail + field_max_length: Maksymalna dÅ‚ugość + field_min_length: Minimalna dÅ‚ugość + field_name: Nazwa + field_new_password: Nowe hasÅ‚o + field_notes: Notatki + field_onthefly: Tworzenie użytkownika w locie + field_parent: Projekt nadrzÄ™dny + field_parent_title: Strona rodzica + field_password: HasÅ‚o + field_password_confirmation: Potwierdzenie + field_port: Port + field_possible_values: Możliwe wartoÅ›ci + field_priority: Priorytet + field_project: Projekt + field_redirect_existing_links: Przekierowanie istniejÄ…cych odnoÅ›ników + field_regexp: Wyrażenie regularne + field_role: Rola + field_searchable: Przeszukiwalne + field_spent_on: Data + field_start_date: Data rozpoczÄ™cia + field_start_page: Strona startowa + field_status: Status + field_subject: Temat + field_subproject: Podprojekt + field_summary: Podsumowanie + field_time_zone: Strefa czasowa + field_title: TytuÅ‚ + field_tracker: Typ zagadnienia + field_type: Typ + field_updated_on: Data modyfikacji + field_url: URL + field_user: Użytkownik + field_value: Wartość + field_version: Wersja + field_vf_personnel: Personel + field_vf_watcher: Obserwator + general_csv_decimal_separator: ',' + general_csv_encoding: UTF-8 + general_csv_separator: ';' + general_first_day_of_week: '1' + general_lang_name: 'Polski' + general_pdf_encoding: UTF-8 + general_text_No: 'Nie' + general_text_Yes: 'Tak' + general_text_no: 'nie' + general_text_yes: 'tak' + + label_activity: Aktywność + label_add_another_file: Dodaj kolejny plik + label_add_note: Dodaj notatkÄ™ + label_added: dodane + label_added_time_by: "Dodane przez %{author} %{age} temu" + label_administration: Administracja + label_age: Wiek + label_ago: dni temu + label_all: wszystko + label_all_time: caÅ‚y czas + label_all_words: Wszystkie sÅ‚owa + label_and_its_subprojects: "%{value} i podprojekty" + label_applied_status: Stosowany status + label_assigned_to_me_issues: Zagadnienia przypisane do mnie + label_associated_revisions: Skojarzone rewizje + label_attachment: Plik + label_attachment_delete: UsuÅ„ plik + label_attachment_new: Nowy plik + label_attachment_plural: Pliki + label_attribute: Atrybut + label_attribute_plural: Atrybuty + label_auth_source: Tryb uwierzytelniania + label_auth_source_new: Nowy tryb uwierzytelniania + label_auth_source_plural: Tryby uwierzytelniania + label_authentication: Uwierzytelnianie + label_blocked_by: blokowane przez + label_blocks: blokuje + label_board: Forum + label_board_new: Nowe forum + label_board_plural: Fora + label_boolean: Wartość logiczna + label_browse: PrzeglÄ…d + label_bulk_edit_selected_issues: Zbiorowa edycja zagadnieÅ„ + label_calendar: Kalendarz + label_change_plural: Zmiany + label_change_properties: ZmieÅ„ wÅ‚aÅ›ciwoÅ›ci + label_change_status: Status zmian + label_change_view_all: Pokaż wszystkie zmiany + label_changes_details: Szczegóły wszystkich zmian + label_changeset_plural: Zestawienia zmian + label_chronological_order: W kolejnoÅ›ci chronologicznej + label_closed_issues: zamkniÄ™te + label_closed_issues_plural234: zamkniÄ™te + label_closed_issues_plural5: zamkniÄ™te + label_closed_issues_plural: zamkniÄ™te + label_x_open_issues_abbr_on_total: + zero: 0 otwartych / %{total} + one: 1 otwarty / %{total} + few: "%{count} otwarte / %{total}" + other: "%{count} otwartych / %{total}" + label_x_open_issues_abbr: + zero: 0 otwartych + one: 1 otwarty + few: "%{count} otwarte" + other: "%{count} otwartych" + label_x_closed_issues_abbr: + zero: 0 zamkniÄ™tych + one: 1 zamkniÄ™ty + few: "%{count} zamkniÄ™te" + other: "%{count} zamkniÄ™tych" + label_comment: Komentarz + label_comment_add: Dodaj komentarz + label_comment_added: Komentarz dodany + label_comment_delete: UsuÅ„ komentarze + label_comment_plural234: Komentarze + label_comment_plural5: Komentarzy + label_comment_plural: Komentarze + label_x_comments: + zero: brak komentarzy + one: 1 komentarz + few: "%{count} komentarze" + other: "%{count} komentarzy" + label_commits_per_author: Zatwierdzenia wedÅ‚ug autorów + label_commits_per_month: Zatwierdzenia wedÅ‚ug miesiÄ™cy + label_confirmation: Potwierdzenie + label_contains: zawiera + label_copied: skopiowano + label_copy_workflow_from: Kopiuj przepÅ‚yw pracy z + label_current_status: Obecny status + label_current_version: Obecna wersja + label_custom_field: Pole niestandardowe + label_custom_field_new: Nowe pole niestandardowe + label_custom_field_plural: Pola niestandardowe + label_date: Data + label_date_from: Od + label_date_range: Zakres dat + label_date_to: Do + label_day_plural: dni + label_default: DomyÅ›lne + label_default_columns: DomyÅ›lne kolumny + label_deleted: usuniÄ™te + label_details: Szczegóły + label_diff_inline: w linii + label_diff_side_by_side: obok siebie + label_disabled: zablokowany + label_display_per_page: "Na stronie: %{value}" + label_document: Dokument + label_document_added: Dodano dokument + label_document_new: Nowy dokument + label_document_plural: Dokumenty + label_downloads_abbr: Pobieranie + label_duplicated_by: zduplikowane przez + label_duplicates: duplikuje + label_end_to_end: koniec do koÅ„ca + label_end_to_start: koniec do poczÄ…tku + label_enumeration_new: Nowa wartość + label_enumerations: Wyliczenia + label_environment: Åšrodowisko + label_equals: równa siÄ™ + label_example: PrzykÅ‚ad + label_export_to: Eksportuj do + label_f_hour: "%{value} godzina" + label_f_hour_plural: "%{value} godzin" + label_feed_plural: Ilość Atom + label_feeds_access_key_created_on: "Klucz dostÄ™pu do kanaÅ‚u Atom stworzony %{value} temu" + label_file_added: Dodano plik + label_file_plural: Pliki + label_filter_add: Dodaj filtr + label_filter_plural: Filtry + label_float: Liczba zmiennoprzecinkowa + label_follows: nastÄ™puje po + label_gantt: Gantt + label_general: Ogólne + label_generate_key: Wygeneruj klucz + label_help: Pomoc + label_history: Historia + label_home: Główna + label_in: w + label_in_less_than: mniejsze niż + label_in_more_than: wiÄ™ksze niż + label_incoming_emails: PrzychodzÄ…ca poczta elektroniczna + label_index_by_date: Indeks wg daty + label_index_by_title: Indeks + label_information: Informacja + label_information_plural: Informacje + label_integer: Liczba caÅ‚kowita + label_internal: WewnÄ™trzny + label_issue: Zagadnienie + label_issue_added: Dodano zagadnienie + label_issue_category: Kategoria zagadnienia + label_issue_category_new: Nowa kategoria + label_issue_category_plural: Kategorie zagadnieÅ„ + label_issue_new: Nowe zagadnienie + label_issue_plural: Zagadnienia + label_issue_status: Status zagadnienia + label_issue_status_new: Nowy status + label_issue_status_plural: Statusy zagadnieÅ„ + label_issue_tracking: Åšledzenie zagadnieÅ„ + label_issue_updated: Uaktualniono zagadnienie + label_issue_view_all: Zobacz wszystkie zagadnienia + label_issue_watchers: Obserwatorzy + label_issues_by: "Zagadnienia wprowadzone przez %{value}" + label_jump_to_a_project: Skocz do projektu... + label_language_based: Na podstawie jÄ™zyka + label_last_changes: "ostatnie %{count} zmian" + label_last_login: Ostatnie połączenie + label_last_month: ostatni miesiÄ…c + label_last_n_days: "ostatnie %{count} dni" + label_last_week: ostatni tydzieÅ„ + label_latest_revision: Najnowsza rewizja + label_latest_revision_plural: Najnowsze rewizje + label_ldap_authentication: Uwierzytelnianie LDAP + label_less_than_ago: dni temu + label_list: Lista + label_loading: Åadowanie... + label_logged_as: Zalogowany jako + label_login: Login + label_logout: Wylogowanie + label_max_size: Maksymalny rozmiar + label_me: ja + label_member: Uczestnik + label_member_new: Nowy uczestnik + label_member_plural: Uczestnicy + label_message_last: Ostatnia wiadomość + label_message_new: Nowa wiadomość + label_message_plural: WiadomoÅ›ci + label_message_posted: Dodano wiadomość + label_min_max_length: Min - Maks dÅ‚ugość + label_modified: zmodyfikowane + label_module_plural: ModuÅ‚y + label_month: MiesiÄ…c + label_months_from: miesiÄ…ce od + label_more: WiÄ™cej + label_more_than_ago: dni od teraz + label_my_account: Moje konto + label_my_page: Moja strona + label_my_projects: Moje projekty + label_new: Nowy + label_new_statuses_allowed: Uprawnione nowe statusy + label_news: Komunikat + label_news_added: Dodano komunikat + label_news_latest: Ostatnie komunikaty + label_news_new: Dodaj komunikat + label_news_plural: Komunikaty + label_news_view_all: Pokaż wszystkie komunikaty + label_next: NastÄ™pne + label_no_change_option: (Bez zmian) + label_no_data: Brak danych do pokazania + label_nobody: nikt + label_none: brak + label_not_contains: nie zawiera + label_not_equals: różni siÄ™ + label_open_issues: otwarte + label_open_issues_plural234: otwarte + label_open_issues_plural5: otwartych + label_open_issues_plural: otwarte + label_optional_description: Opcjonalny opis + label_options: Opcje + label_overall_activity: Ogólna aktywność + label_overview: PrzeglÄ…d + label_password_lost: Zapomniane hasÅ‚o + label_per_page: Na stronÄ™ + label_permissions: Uprawnienia + label_permissions_report: Raport uprawnieÅ„ + label_personalize_page: Personalizuj tÄ™ stronÄ™ + label_planning: Planowanie + label_please_login: Zaloguj siÄ™ + label_plugins: Wtyczki + label_precedes: poprzedza + label_preferences: Preferencje + label_preview: PodglÄ…d + label_previous: Poprzednie + label_project: Projekt + label_project_all: Wszystkie projekty + label_project_latest: Ostatnie projekty + label_project_new: Nowy projekt + label_project_plural234: Projekty + label_project_plural5: Projektów + label_project_plural: Projekty + label_x_projects: + zero: brak projektów + one: 1 projekt + few: "%{count} projekty" + other: "%{count} projektów" + label_public_projects: Projekty publiczne + label_query: Kwerenda + label_query_new: Nowa kwerenda + label_query_plural: Kwerendy + label_read: Czytanie... + label_register: Rejestracja + label_registered_on: Zarejestrowany + label_registration_activation_by_email: aktywacja konta przez e-mail + label_registration_automatic_activation: automatyczna aktywacja kont + label_registration_manual_activation: manualna aktywacja kont + label_related_issues: PowiÄ…zane zagadnienia + label_relates_to: powiÄ…zane z + label_relation_delete: UsuÅ„ powiÄ…zanie + label_relation_new: Nowe powiÄ…zanie + label_renamed: zmieniono nazwÄ™ + label_reply_plural: Odpowiedzi + label_report: Raport + label_report_plural: Raporty + label_reported_issues: Wprowadzone zagadnienia + label_repository: Repozytorium + label_repository_plural: Repozytoria + label_result_plural: Rezultatów + label_reverse_chronological_order: W kolejnoÅ›ci odwrotnej do chronologicznej + label_revision: Rewizja + label_revision_plural: Rewizje + label_roadmap: Mapa + label_roadmap_due_in: W czasie + label_roadmap_no_issues: Brak zagadnieÅ„ do tej wersji + label_roadmap_overdue: "%{value} spóźnienia" + label_role: Rola + label_role_and_permissions: Role i uprawnienia + label_role_new: Nowa rola + label_role_plural: Role + label_scm: SCM + label_search: Szukaj + label_search_titles_only: Przeszukuj tylko tytuÅ‚y + label_send_information: WyÅ›lij informacjÄ™ użytkownikowi + label_send_test_email: WyÅ›lij próbny e-mail + label_settings: Ustawienia + label_show_completed_versions: Pokaż kompletne wersje + label_sort_by: "Sortuj po %{value}" + label_sort_higher: Do góry + label_sort_highest: PrzesuÅ„ na górÄ™ + label_sort_lower: Do doÅ‚u + label_sort_lowest: PrzesuÅ„ na dół + label_spent_time: Przepracowany czas + label_start_to_end: poczÄ…tek do koÅ„ca + label_start_to_start: poczÄ…tek do poczÄ…tku + label_statistics: Statystyki + label_stay_logged_in: PozostaÅ„ zalogowany + label_string: Tekst + label_subproject_plural: Podprojekty + label_text: DÅ‚ugi tekst + label_theme: Motyw + label_this_month: ten miesiÄ…c + label_this_week: ten tydzieÅ„ + label_this_year: ten rok + label_time_tracking: Åšledzenie czasu pracy + label_today: dzisiaj + label_topic_plural: Tematy + label_total: Ogółem + label_tracker: Typ zagadnienia + label_tracker_new: Nowy typ zagadnienia + label_tracker_plural: Typy zagadnieÅ„ + label_updated_time: "Zaktualizowane %{value} temu" + label_used_by: Używane przez + label_user: Użytkownik + label_user_mail_no_self_notified: "Nie chcÄ™ powiadomieÅ„ o zmianach, które sam wprowadzam." + label_user_mail_option_all: "Dla każdego zdarzenia w każdym moim projekcie" + label_user_mail_option_selected: "Dla każdego zdarzenia w wybranych projektach..." + label_user_new: Nowy użytkownik + label_user_plural: Użytkownicy + label_version: Wersja + label_version_new: Nowa wersja + label_version_plural: Wersje + label_view_diff: Pokaż różnice + label_view_revisions: Pokaż rewizje + label_watched_issues: Obserwowane zagadnienia + label_week: TydzieÅ„ + label_wiki: Wiki + label_wiki_edit: Edycja wiki + label_wiki_edit_plural: Edycje wiki + label_wiki_page: Strona wiki + label_wiki_page_plural: Strony wiki + label_workflow: PrzepÅ‚yw pracy + label_year: Rok + label_yesterday: wczoraj + mail_body_account_activation_request: "Zarejestrowano nowego użytkownika: (%{value}). Konto oczekuje na twoje zatwierdzenie:" + mail_body_account_information: Twoje konto + mail_body_account_information_external: "Możesz użyć Twojego konta %{value} do zalogowania." + mail_body_lost_password: 'W celu zmiany swojego hasÅ‚a użyj poniższego odnoÅ›nika:' + mail_body_register: 'W celu aktywacji Twojego konta, użyj poniższego odnoÅ›nika:' + mail_body_reminder: "Wykaz przypisanych do Ciebie zagadnieÅ„, których termin wypada w ciÄ…gu nastÄ™pnych %{count} dni" + mail_subject_account_activation_request: "Zapytanie aktywacyjne konta %{value}" + mail_subject_lost_password: "Twoje hasÅ‚o do %{value}" + mail_subject_register: "Aktywacja konta w %{value}" + mail_subject_reminder: "Uwaga na terminy, masz zagadnienia do obsÅ‚użenia w ciÄ…gu nastÄ™pnych %{count} dni! (%{days})" + notice_account_activated: Twoje konto zostaÅ‚o aktywowane. Możesz siÄ™ zalogować. + notice_account_invalid_creditentials: ZÅ‚y użytkownik lub hasÅ‚o + notice_account_lost_email_sent: E-mail z instrukcjami zmiany hasÅ‚a zostaÅ‚ wysÅ‚any do Ciebie. + notice_account_password_updated: HasÅ‚o prawidÅ‚owo zmienione. + notice_account_pending: "Twoje konto zostaÅ‚o utworzone i oczekuje na zatwierdzenie administratora." + notice_account_unknown_email: Nieznany użytkownik. + notice_account_updated: Konto prawidÅ‚owo zaktualizowane. + notice_account_wrong_password: ZÅ‚e hasÅ‚o + notice_can_t_change_password: To konto ma zewnÄ™trzne źródÅ‚o uwierzytelniania. Nie możesz zmienić hasÅ‚a. + notice_default_data_loaded: DomyÅ›lna konfiguracja zostaÅ‚a pomyÅ›lnie zaÅ‚adowana. + notice_email_error: "WystÄ…piÅ‚ błąd w trakcie wysyÅ‚ania e-maila (%{value})" + notice_email_sent: "E-mail zostaÅ‚ wysÅ‚any do %{value}" + notice_failed_to_save_issues: "Błąd podczas zapisu zagadnieÅ„ %{count} z %{total} zaznaczonych: %{ids}." + notice_feeds_access_key_reseted: Twój klucz dostÄ™pu do kanaÅ‚u Atom zostaÅ‚ zresetowany. + notice_file_not_found: Strona do której próbujesz siÄ™ dostać nie istnieje lub zostaÅ‚a usuniÄ™ta. + notice_locking_conflict: Dane poprawione przez innego użytkownika. + notice_no_issue_selected: "Nie wybrano zagadnienia! Zaznacz zagadnienie, które chcesz edytować." + notice_not_authorized: Nie posiadasz autoryzacji do oglÄ…dania tej strony. + notice_successful_connection: Udane nawiÄ…zanie połączenia. + notice_successful_create: Utworzenie zakoÅ„czone pomyÅ›lnie. + notice_successful_delete: UsuniÄ™cie zakoÅ„czone pomyÅ›lnie. + notice_successful_update: Uaktualnienie zakoÅ„czone pomyÅ›lnie. + notice_unable_delete_version: Nie można usunąć wersji + permission_add_issue_notes: Dodawanie notatek + permission_add_issue_watchers: Dodawanie obserwatorów + permission_add_issues: Dodawanie zagadnieÅ„ + permission_add_messages: Dodawanie wiadomoÅ›ci + permission_browse_repository: PrzeglÄ…danie repozytorium + permission_comment_news: Komentowanie komunikatów + permission_commit_access: Wykonywanie zatwierdzeÅ„ + permission_delete_issues: Usuwanie zagadnieÅ„ + permission_delete_messages: Usuwanie wiadomoÅ›ci + permission_delete_wiki_pages: Usuwanie stron wiki + permission_delete_wiki_pages_attachments: Usuwanie załączników + permission_delete_own_messages: Usuwanie wÅ‚asnych wiadomoÅ›ci + permission_edit_issue_notes: Edycja notatek + permission_edit_issues: Edycja zagadnieÅ„ + permission_edit_messages: Edycja wiadomoÅ›ci + permission_edit_own_issue_notes: Edycja wÅ‚asnych notatek + permission_edit_own_messages: Edycja wÅ‚asnych wiadomoÅ›ci + permission_edit_own_time_entries: Edycja wÅ‚asnego dziennika + permission_edit_project: Edycja projektów + permission_edit_time_entries: Edycja wpisów dziennika + permission_edit_wiki_pages: Edycja stron wiki + permission_log_time: Zapisywanie przepracowanego czasu + permission_manage_boards: ZarzÄ…dzanie forami + permission_manage_categories: ZarzÄ…dzanie kategoriami zagadnieÅ„ + permission_manage_files: ZarzÄ…dzanie plikami + permission_manage_issue_relations: ZarzÄ…dzanie powiÄ…zaniami zagadnieÅ„ + permission_manage_members: ZarzÄ…dzanie uczestnikami + permission_manage_news: ZarzÄ…dzanie komunikatami + permission_manage_public_queries: ZarzÄ…dzanie publicznymi kwerendami + permission_manage_repository: ZarzÄ…dzanie repozytorium + permission_manage_versions: ZarzÄ…dzanie wersjami + permission_manage_wiki: ZarzÄ…dzanie wiki + permission_move_issues: Przenoszenie zagadnieÅ„ + permission_protect_wiki_pages: Blokowanie stron wiki + permission_rename_wiki_pages: Zmiana nazw stron wiki + permission_save_queries: Zapisywanie kwerend + permission_select_project_modules: Wybieranie modułów projektu + permission_view_calendar: PodglÄ…d kalendarza + permission_view_changesets: PodglÄ…d zmian + permission_view_documents: PodglÄ…d dokumentów + permission_view_files: PodglÄ…d plików + permission_view_gantt: PodglÄ…d diagramu Gantta + permission_view_issue_watchers: PodglÄ…d listy obserwatorów + permission_view_messages: PodglÄ…d wiadomoÅ›ci + permission_view_time_entries: PodglÄ…d przepracowanego czasu + permission_view_wiki_edits: PodglÄ…d historii wiki + permission_view_wiki_pages: PodglÄ…d wiki + project_module_boards: Fora + project_module_documents: Dokumenty + project_module_files: Pliki + project_module_issue_tracking: Åšledzenie zagadnieÅ„ + project_module_news: Komunikaty + project_module_repository: Repozytorium + project_module_time_tracking: Åšledzenie czasu pracy + project_module_wiki: Wiki + setting_activity_days_default: Dni wyÅ›wietlane w aktywnoÅ›ci projektu + setting_app_subtitle: PodtytuÅ‚ aplikacji + setting_app_title: TytuÅ‚ aplikacji + setting_attachment_max_size: Maks. rozm. załącznika + setting_autofetch_changesets: Automatyczne pobieranie zmian + setting_autologin: Automatyczne logowanie + setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) + setting_commit_fix_keywords: SÅ‚owo zmieniajÄ…ce status + setting_commit_ref_keywords: SÅ‚owa tworzÄ…ce powiÄ…zania + setting_cross_project_issue_relations: Zezwól na powiÄ…zania zagadnieÅ„ miÄ™dzy projektami + setting_date_format: Format daty + setting_default_language: DomyÅ›lny jÄ™zyk + setting_default_projects_public: Nowe projekty sÄ… domyÅ›lnie publiczne + setting_display_subprojects_issues: DomyÅ›lnie pokazuj zagadnienia podprojektów w głównym projekcie + setting_emails_footer: Stopka e-mail + setting_enabled_scm: DostÄ™pny SCM + setting_feeds_limit: Limit danych Atom + setting_gravatar_enabled: Używaj ikon użytkowników Gravatar + setting_host_name: Nazwa hosta i Å›cieżka + setting_issue_list_default_columns: DomyÅ›lne kolumny wyÅ›wietlane na liÅ›cie zagadnieÅ„ + setting_issues_export_limit: Limit eksportu zagadnieÅ„ + setting_login_required: Wymagane zalogowanie + setting_mail_from: Adres e-mail wysyÅ‚ki + setting_mail_handler_api_enabled: Uaktywnij usÅ‚ugi sieciowe (WebServices) dla poczty przychodzÄ…cej + setting_mail_handler_api_key: Klucz API + setting_per_page_options: Opcje iloÅ›ci obiektów na stronie + setting_plain_text_mail: tylko tekst (bez HTML) + setting_protocol: Protokół + setting_self_registration: Samodzielna rejestracja użytkowników + setting_sequential_project_identifiers: Generuj sekwencyjne identyfikatory projektów + setting_sys_api_enabled: Włączenie WS do zarzÄ…dzania repozytorium + setting_text_formatting: Formatowanie tekstu + setting_time_format: Format czasu + setting_user_format: Format wyÅ›wietlania użytkownika + setting_welcome_text: Tekst powitalny + setting_wiki_compression: Kompresja historii Wiki + status_active: aktywny + status_locked: zablokowany + status_registered: zarejestrowany + text_are_you_sure: JesteÅ› pewien ? + text_assign_time_entries_to_project: Przypisz wpisy dziennika do projektu + text_caracters_maximum: "%{count} znaków maksymalnie." + text_caracters_minimum: "Musi być nie krótsze niż %{count} znaków." + text_comma_separated: Dozwolone wielokrotne wartoÅ›ci (rozdzielone przecinkami). + text_default_administrator_account_changed: Zmieniono domyÅ›lne hasÅ‚o administratora + text_destroy_time_entries: UsuÅ„ wpisy dziennika + text_destroy_time_entries_question: Przepracowano %{hours} godzin przy zagadnieniu, które chcesz usunąć. Co chcesz zrobić? + text_email_delivery_not_configured: "Dostarczanie poczty elektronicznej nie zostaÅ‚o skonfigurowane, wiÄ™c powiadamianie jest nieaktywne.\nSkonfiguruj serwer SMTP w config/configuration.yml a nastÄ™pnie zrestartuj aplikacjÄ™ i uaktywnij to." + text_enumeration_category_reassign_to: 'ZmieÅ„ przypisanie na tÄ… wartość:' + text_enumeration_destroy_question: "%{count} obiektów jest przypisanych do tej wartoÅ›ci." + text_file_repository_writable: Zapisywalne repozytorium plików + text_issue_added: "Zagadnienie %{id} zostaÅ‚o wprowadzone (przez %{author})." + text_issue_category_destroy_assignments: UsuÅ„ przydziaÅ‚y kategorii + text_issue_category_destroy_question: "Do tej kategorii sÄ… przypisane zagadnienia (%{count}). Co chcesz zrobić?" + text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii + text_issue_updated: "Zagadnienie %{id} zostaÅ‚o zaktualizowane (przez %{author})." + text_issues_destroy_confirmation: 'Czy jesteÅ› pewien, że chcesz usunąć wskazane zagadnienia?' + text_issues_ref_in_commit_messages: OdwoÅ‚ania do zagadnieÅ„ Redmine w komentarzach w repozytorium + text_length_between: "DÅ‚ugość pomiÄ™dzy %{min} i %{max} znaków." + text_load_default_configuration: ZaÅ‚aduj domyÅ›lnÄ… konfiguracjÄ™ + text_min_max_length_info: 0 oznacza brak restrykcji + text_no_configuration_data: "Role użytkowników, typy zagadnieÅ„, statusy zagadnieÅ„ oraz przepÅ‚yw pracy nie zostaÅ‚y jeszcze skonfigurowane.\nWysoce zalecane jest by zaÅ‚adować domyÅ›lnÄ… konfiguracjÄ™. Po zaÅ‚adowaniu bÄ™dzie możliwość edycji tych danych." + text_project_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć ten projekt i wszystkie powiÄ…zane dane? + text_reassign_time_entries: 'Przepnij przepracowany czas do tego zagadnienia:' + text_regexp_info: np. ^[A-Z0-9]+$ + text_repository_usernames_mapping: "Wybierz lub uaktualnij przyporzÄ…dkowanie użytkowników Redmine do użytkowników repozytorium.\nUżytkownicy z takÄ… samÄ… nazwÄ… lub adresem e-mail sÄ… przyporzÄ…dkowani automatycznie." + text_rmagick_available: RMagick dostÄ™pne (opcjonalnie) + text_select_mail_notifications: Zaznacz czynnoÅ›ci przy których użytkownik powinien być powiadomiony e-mailem. + text_select_project_modules: 'Wybierz moduÅ‚y do aktywacji w tym projekcie:' + text_status_changed_by_changeset: "Zastosowane w zmianach %{value}." + text_subprojects_destroy_warning: "Podprojekt(y): %{value} zostanÄ… także usuniÄ™te." + text_tip_issue_begin_day: zadanie zaczynajÄ…ce siÄ™ dzisiaj + text_tip_issue_begin_end_day: zadanie zaczynajÄ…ce i koÅ„czÄ…ce siÄ™ dzisiaj + text_tip_issue_end_day: zadanie koÅ„czÄ…ce siÄ™ dzisiaj + text_tracker_no_workflow: Brak przepÅ‚ywu pracy zdefiniowanego dla tego typu zagadnienia + text_unallowed_characters: Niedozwolone znaki + text_user_mail_option: "W przypadku niezaznaczonych projektów, bÄ™dziesz otrzymywaÅ‚ powiadomienia tylko na temat zagadnieÅ„ które obserwujesz, lub w których bierzesz udziaÅ‚ (np. jesteÅ› autorem lub adresatem)." + text_user_wrote: "%{value} napisaÅ‚(a):" + text_wiki_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć to wiki i całą jego zawartość? + text_workflow_edit: Zaznacz rolÄ™ i typ zagadnienia do edycji przepÅ‚ywu pracy + + label_user_activity: "Aktywność: %{value}" + label_updated_time_by: "Uaktualnione przez %{author} %{age} temu" + text_diff_truncated: '... Ten plik różnic zostaÅ‚ przyciÄ™ty ponieważ jest zbyt dÅ‚ugi.' + setting_diff_max_lines_displayed: Maksymalna liczba linii różnicy do pokazania + text_plugin_assets_writable: Zapisywalny katalog zasobów wtyczek + warning_attachments_not_saved: "%{count} załącznik(ów) nie zostaÅ‚o zapisanych." + field_editable: Edytowalne + label_display: WyglÄ…d + button_create_and_continue: Stwórz i dodaj kolejne + text_custom_field_possible_values_info: 'Każda wartość w osobnej linii' + setting_repository_log_display_limit: Maksymalna liczba rewizji pokazywanych w logu pliku + setting_file_max_size_displayed: Maksymalny rozmiar plików tekstowych osadzanych w stronie + field_watcher: Obserwator + setting_openid: Logowanie i rejestracja przy użyciu OpenID + field_identity_url: Identyfikator OpenID (URL) + label_login_with_open_id_option: albo użyj OpenID + field_content: Treść + label_descending: MalejÄ…co + label_sort: Sortuj + label_ascending: RosnÄ…co + label_date_from_to: Od %{start} do %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Ta strona posiada podstrony (%{descendants}). Co chcesz zrobić? + text_wiki_page_reassign_children: Podepnij je do strony nadrzÄ™dnej wzglÄ™dem usuwanej + text_wiki_page_nullify_children: PrzesuÅ„ je na szczyt hierarchii + text_wiki_page_destroy_children: UsuÅ„ wszystkie podstrony + setting_password_min_length: Minimalna dÅ‚ugość hasÅ‚a + field_group_by: Grupuj wyniki wg + mail_subject_wiki_content_updated: "Strona wiki '%{id}' zostaÅ‚a uaktualniona" + label_wiki_content_added: Dodano stronÄ™ wiki + mail_subject_wiki_content_added: "Strona wiki '%{id}' zostaÅ‚a dodana" + mail_body_wiki_content_added: Strona wiki '%{id}' zostaÅ‚a dodana przez %{author}. + label_wiki_content_updated: Uaktualniono stronÄ™ wiki + mail_body_wiki_content_updated: Strona wiki '%{id}' zostaÅ‚a uaktualniona przez %{author}. + permission_add_project: Tworzenie projektu + setting_new_project_user_role_id: Rola nadawana twórcom projektów, którzy nie posiadajÄ… uprawnieÅ„ administatora + label_view_all_revisions: Pokaż wszystkie rewizje + label_tag: SÅ‚owo kluczowe + label_branch: Gałąź + error_no_tracker_in_project: Projekt nie posiada powiÄ…zanych typów zagadnieÅ„. Sprawdź ustawienia projektu. + error_no_default_issue_status: Nie zdefiniowano domyÅ›lnego statusu zagadnieÅ„. Sprawdź konfiguracjÄ™ (Przejdź do "Administracja -> Statusy zagadnieÅ„"). + text_journal_changed: "Zmieniono %{label} z %{old} na %{new}" + text_journal_set_to: "Ustawiono %{label} na %{value}" + text_journal_deleted: "UsuniÄ™to %{label} (%{old})" + label_group_plural: Grupy + label_group: Grupa + label_group_new: Nowa grupa + label_time_entry_plural: Przepracowany czas + text_journal_added: "Dodano %{label} %{value}" + field_active: Aktywne + enumeration_system_activity: Aktywność systemowa + button_copy_and_follow: Kopiuj i przejdź do kopii zagadnienia + button_duplicate: Duplikuj + button_move_and_follow: PrzenieÅ› i przejdź do zagadnienia + button_show: Pokaż + error_can_not_archive_project: Ten projekt nie może zostać zarchiwizowany + error_can_not_reopen_issue_on_closed_version: Zagadnienie przydzielone do zakoÅ„czonej wersji nie może zostać ponownie otwarte + error_issue_done_ratios_not_updated: "% wykonania zagadnienia nie zostaÅ‚ uaktualniony." + error_workflow_copy_source: ProszÄ™ wybrać źródÅ‚owy typ zagadnienia lub rolÄ™ + error_workflow_copy_target: ProszÄ™ wybrać docelowe typ(y) zagadnieÅ„ i rolÄ™(e) + field_sharing: Współdzielenie + label_api_access_key: Klucz dostÄ™pu do API + label_api_access_key_created_on: Klucz dostÄ™pu do API zostaÅ‚ utworzony %{value} temu + label_close_versions: Zamknij ukoÅ„czone wersje + label_copy_same_as_target: Jak cel + label_copy_source: ŹródÅ‚o + label_copy_target: Cel + label_display_used_statuses_only: WyÅ›wietlaj tylko statusy używane przez ten typ zagadnienia + label_feeds_access_key: Klucz dostÄ™pu do kanaÅ‚u Atom + label_missing_api_access_key: Brakuje klucza dostÄ™pu do API + label_missing_feeds_access_key: Brakuje klucza dostÄ™pu do kanaÅ‚u Atom + label_revision_id: Rewizja %{value} + label_subproject_new: Nowy podprojekt + label_update_issue_done_ratios: Uaktualnij % wykonania + label_user_anonymous: Anonimowy + label_version_sharing_descendants: Z podprojektami + label_version_sharing_hierarchy: Z hierarchiÄ… projektów + label_version_sharing_none: Brak współdzielenia + label_version_sharing_system: Ze wszystkimi projektami + label_version_sharing_tree: Z drzewem projektów + notice_api_access_key_reseted: Twój klucz dostÄ™pu do API zostaÅ‚ zresetowany. + notice_issue_done_ratios_updated: Uaktualnienie % wykonania zakoÅ„czone pomyÅ›lnie. + permission_add_subprojects: Tworzenie podprojektów + permission_delete_issue_watchers: UsuÅ„ obserwatorów + permission_view_issues: PrzeglÄ…danie zagadnieÅ„ + setting_default_projects_modules: DomyÅ›lnie włączone moduÅ‚y dla nowo tworzonych projektów + setting_gravatar_default: DomyÅ›lny obraz Gravatar + setting_issue_done_ratio: Obliczaj postÄ™p realizacji zagadnieÅ„ za pomocÄ… + setting_issue_done_ratio_issue_field: "% Wykonania zagadnienia" + setting_issue_done_ratio_issue_status: Statusu zagadnienia + setting_mail_handler_body_delimiters: Przycinaj e-maile po jednej z tych linii + setting_rest_api_enabled: Uaktywnij usÅ‚ugÄ™ sieciowÄ… REST + setting_start_of_week: Pierwszy dzieÅ„ tygodnia + text_line_separated: Dozwolone jest wiele wartoÅ›ci (każda wartość w osobnej linii). + text_own_membership_delete_confirmation: |- + Masz zamiar usunąć niektóre lub wszystkie swoje uprawnienia. Po wykonaniu tej czynnoÅ›ci możesz utracić możliwoÅ›ci edycji tego projektu. + Czy na pewno chcesz kontynuować? + version_status_closed: zamkniÄ™ta + version_status_locked: zablokowana + version_status_open: otwarta + + label_board_sticky: Przyklejona + label_board_locked: ZamkniÄ™ta + permission_export_wiki_pages: Eksport stron wiki + permission_manage_project_activities: ZarzÄ…dzanie aktywnoÅ›ciami projektu + setting_cache_formatted_text: Buforuj sformatowany tekst + error_unable_delete_issue_status: Nie można usunąć statusu zagadnienia + label_profile: Profil + permission_manage_subtasks: ZarzÄ…dzanie podzagadnieniami + field_parent_issue: Zagadnienie nadrzÄ™dne + label_subtask_plural: Podzagadnienia + label_project_copy_notifications: WyÅ›lij powiadomienia e-mailowe przy kopiowaniu projektu + error_can_not_delete_custom_field: Nie można usunąć tego pola + error_unable_to_connect: Nie można połączyć (%{value}) + error_can_not_remove_role: Ta rola przypisana jest niektórym użytkownikom i nie może zostać usuniÄ™ta. + error_can_not_delete_tracker: Ten typ przypisany jest do części zagadnieÅ„ i nie może zostać usuniÄ™ty. + field_principal: PrzeÅ‚ożony + label_my_page_block: Elementy + notice_failed_to_save_members: "Nie można zapisać uczestników: %{errors}." + text_zoom_out: Zmniejsz + text_zoom_in: PowiÄ™ksz + notice_unable_delete_time_entry: Nie można usunąć wpisu z dziennika. + label_overall_spent_time: Przepracowany czas + field_time_entries: Dziennik + project_module_gantt: Diagram Gantta + project_module_calendar: Kalendarz + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Tylko to, czego jestem wÅ‚aÅ›cicielem (autorem) + setting_default_notification_option: DomyÅ›lna opcja powiadomieÅ„ + label_user_mail_option_only_my_events: "Tylko to, co obserwujÄ™ lub w czym biorÄ™ udziaÅ‚" + label_user_mail_option_only_assigned: "Tylko to, do czego jestem przypisany" + label_user_mail_option_none: "Brak powiadomieÅ„" + field_member_of_group: Grupa osoby przypisanej + field_assigned_to_role: Rola osoby przypisanej + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Szukaj użytkownika lub grupy:" + label_user_search: "Szukaj użytkownika:" + field_visible: Widoczne + setting_commit_logtime_activity_id: Aktywność dla Å›ledzonego czasu + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Włącz Å›ledzenie czasu + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maksymalna liczba elementów wyÅ›wietlanych na diagramie Gantta + field_warn_on_leaving_unsaved: Ostrzegaj mnie, gdy opuszczam stronÄ™ z niezapisanym tekstem + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: Moje kwerendy + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Dodano komentarz do komunikatu + button_expand_all: RozwiÅ„ wszystkie + button_collapse_all: ZwiÅ„ wszystkie + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonimowy + label_role_non_member: Bez roli + label_issue_note_added: Dodano notatkÄ™ + label_issue_status_updated: Uaktualniono status + label_issue_priority_updated: Uaktualniono priorytet + label_issues_visibility_own: Utworzone lub przypisane do użytkownika + field_issues_visibility: Widoczne zagadnienia + label_issues_visibility_all: Wszystkie + permission_set_own_issues_private: Ustawianie wÅ‚asnych zagadnieÅ„ jako prywatne/publiczne + field_is_private: Prywatne + permission_set_issues_private: Ustawianie zagadnieÅ„ jako prywatne/publiczne + label_issues_visibility_public: Wszystkie nie prywatne + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Kodowanie komentarzy zatwierdzeÅ„ + 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: Polecenie + text_scm_command_version: Wersja + label_git_report_last_commit: Report last commit for files and directories + notice_issue_successful_create: Issue %{id} created. + label_between: pomiÄ™dzy + setting_issue_group_assignment: Zezwól przypisywać zagadnienia do grup + 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: DostÄ™pne kolumny + 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: Wybrane kolumny + 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: Użyj bieżącej daty jako daty rozpoczÄ™cia nowych zagadnieÅ„ + button_edit_section: Edit this section + setting_repositories_encodings: Kodowanie znaków załączników i repozytoriów + 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 zagadnienie + one: 1 zagadnienie + other: "%{count} zagadnienia" + 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: 'Dozwolone maÅ‚e litery (a-z), liczby i myÅ›lniki.
    Raz zapisany, identyfikator nie może być zmieniony.' + field_multiple: Multiple values + setting_commit_cross_project_ref: Zezwól na odwołania do innych projektów i zamykanie zagadnień innych projektów + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Zarządzanie powiązanymi zagadnieniami + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Wyszukaj obserwatorów do dodania + notice_account_deleted: Twoje konto zostało trwale usunięte. + setting_unsubscribe: Zezwól użytkownikom usuwać swoje konta + button_delete_my_account: Usuń moje konto + text_account_destroy_confirmation: |- + Czy jesteś pewien? + Twoje konto zostanie trwale usunięte, bez możliwości przywrócenia go. + error_session_expired: Twoja sesja wygasła. Zaloguj się ponownie. + text_session_expiration_settings: "Uwaga: zmiana tych ustawień może spowodować przeterminowanie sesji obecnie zalogowanych użytkowników, w tym twojej." + setting_session_lifetime: Maksymalny czas życia sesji + setting_session_timeout: Maksymalny czas życia nieaktywnej sesji + label_session_expiration: Przeterminowywanie sesji + permission_close_project: Zamykanie / otwieranie projektów + label_show_closed_projects: Przeglądanie zamkniętych projektów + button_close: Zamknij projekt + button_reopen: Otwórz projekt + project_status_active: aktywny + project_status_closed: zamknięty + project_status_archived: zarchiwizowany + text_project_closed: Ten projekt jest zamknięty i dostępny tylko do odczytu. + notice_user_successful_create: Utworzono użytkownika %{id}. + field_core_fields: Pola standardowe + field_timeout: Limit czasu (w sekundach) + setting_thumbnails_enabled: Wyświetlaj miniatury załączników + setting_thumbnails_size: Rozmiar minuatury (w pikselach) + label_status_transitions: Przejścia między statusami + label_fields_permissions: Uprawnienia do pól + label_readonly: Tylko do odczytu + label_required: Wymagane + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Kopiuj podzagadnienia + label_copied_to: skopiowane do + label_copied_from: skopiowane z + label_any_issues_in_project: dowolne zagadnienie w projekcie + label_any_issues_not_in_project: dowolne zagadnienie w innym projekcie + field_private_notes: Prywatne notatki + permission_view_private_notes: Podgląd prywatnych notatek + permission_set_notes_private: Ustawianie notatek jako prywatnych + label_no_issues_in_project: brak zagadnień w projekcie + label_any: wszystko + label_last_n_weeks: ostatnie %{count} tygodnie + setting_cross_project_subtasks: Powiązania zagadnień między projektami + label_cross_project_descendants: Z podprojektami + label_cross_project_tree: Z drzewem projektów + label_cross_project_hierarchy: Z hierarchią projektów + label_cross_project_system: Ze wszystkimi projektami + button_hide: Ukryj + setting_non_working_week_days: Dni nie-robocze + label_in_the_next_days: w ciągu następnych dni + label_in_the_past_days: w ciągu poprzednich dni + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Dodawanie dokumentów + permission_edit_documents: Edycja dokumentów + permission_delete_documents: Usuwanie dokumentów + label_gantt_progress_line: Linia postępu + setting_jsonp_enabled: Uaktywnij wsparcie dla JSONP + field_inherit_members: Dziedziczenie członków + field_closed_on: Data zamknięcia + field_generate_password: Wygeneruj hasło + setting_default_projects_tracker_ids: Domyślne typy zagadnień dla nowych projektów + label_total_time: Ogółem + text_scm_config: Możesz skonfigurować polecenia SCM w pliku config/configuration.yml. Zrestartuj aplikację po wykonaniu zmian. + text_scm_command_not_available: Polecenie SCM nie jest dostępne. Proszę sprawdzić ustawienia w panelu administracyjnym. + setting_emails_header: Nagłówek e-maili + notice_account_not_activated_yet: Jeszcze nie aktywowałeś swojego konta. Jeśli chcesz + otrzymać nowy e-mail aktywanyjny, kliknij tutaj. + notice_account_locked: Twoje konto jest zablokowane. + notice_account_register_done: Konto zostało pomyślnie utworzone. E-mail zawierający + instrukcję aktywacji konta został wysłany na adres %{email}. + label_hidden: Hidden + label_visibility_private: tylko dla mnie + label_visibility_roles: tylko dla ról + label_visibility_public: dla wszystkich + field_must_change_passwd: Musi zmienić hasło przy następnym logowaniu + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Wyklucz załączniki wg nazwy + text_convert_available: Konwersja przez ImageMagick dostępna (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Wymuś domyślny język dla anonimowych użytkowników + setting_force_default_language_for_loggedin: Wymuś domyślny język dla zalogowanych użytkowników + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/22629c3ce001e5a68327c24a4b23675476508e16.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22629c3ce001e5a68327c24a4b23675476508e16.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,32 @@ +<%= error_messages_for 'time_entry' %> +<%= back_url_hidden_field_tag %> + +
    + <% if @time_entry.new_record? %> + <% if params[:project_id] || @time_entry.issue %> + <%= f.hidden_field :project_id %> + <% else %> +

    <%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).all, :selected => @time_entry.project), :required => true %>

    + <% end %> + <% end %> +

    + <%= f.text_field :issue_id, :size => 6 %> + <%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %> +

    +

    <%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %>

    +

    <%= f.text_field :hours, :size => 6, :required => true %>

    +

    <%= f.text_field :comments, :size => 100, :maxlength => 255 %>

    +

    <%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %>

    + <% @time_entry.custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :time_entry, value %>

    + <% end %> + <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %> +
    + +<%= javascript_tag do %> + observeAutocompleteField('time_entry_issue_id', '<%= escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (@project ? nil : 'all'))%>', { + select: function(event, ui) { + $('#time_entry_issue').text(ui.item.label); + } + }); +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/226ca6df54d2f722db0a366c7bc4bc8b7ec109c6.svn-base --- a/.svn/pristine/22/226ca6df54d2f722db0a366c7bc4bc8b7ec109c6.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 SubclassFactory - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def get_subclass(class_name) - klass = nil - begin - klass = class_name.to_s.classify.constantize - rescue - # invalid class name - end - unless subclasses.include? klass - klass = nil - end - klass - end - - # Returns an instance of the given subclass name - def new_subclass_instance(class_name, *args) - klass = get_subclass(class_name) - if klass - klass.new(*args) - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/228763af0fab9d92d601d760e23db085d02a829d.svn-base --- a/.svn/pristine/22/228763af0fab9d92d601d760e23db085d02a829d.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -
    - « - <% unless @changeset.previous.nil? -%> - <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %> - <% else -%> - <%= l(:label_previous) %> - <% end -%> -| - <% unless @changeset.next.nil? -%> - <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %> - <% else -%> - <%= l(:label_next) %> - <% end -%> - »  - - <%= form_tag({:controller => 'repositories', - :action => 'revision', - :id => @project, - :repository_id => @repository.identifier_param, - :rev => nil}, - :method => :get) do %> - <%= text_field_tag 'rev', @rev, :size => 8 %> - <%= submit_tag 'OK', :name => nil %> - <% end %> -
    - -

    <%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %>

    - -<% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %> - - <% if @changeset.scmid.present? %> - - - - <% end %> - <% if @changeset.parents.present? %> - - - - - <% end %> - <% if @changeset.children.present? %> - - - - - <% end %> -
    ID<%= h(@changeset.scmid) %>
    <%= l(:label_parent_revision) %> - <%= @changeset.parents.collect{ - |p| link_to_revision(p, @repository, :text => format_revision(p)) - }.join(", ").html_safe %> -
    <%= l(:label_child_revision) %> - <%= @changeset.children.collect{ - |p| link_to_revision(p, @repository, :text => format_revision(p)) - }.join(", ").html_safe %> -
    -<% end %> - -

    - -<%= authoring(@changeset.committed_on, @changeset.author) %> - -

    - -<%= textilizable @changeset.comments %> - -<% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %> - <%= render :partial => 'related_issues' %> -<% end %> - -<% if User.current.allowed_to?(:browse_repository, @project) %> -

    <%= l(:label_attachment_plural) %>

    -
      -
    • <%= l(:label_added) %>
    • -
    • <%= l(:label_modified) %>
    • -
    • <%= l(:label_copied) %>
    • -
    • <%= l(:label_renamed) %>
    • -
    • <%= l(:label_deleted) %>
    • -
    - -

    <%= link_to(l(:label_view_diff), - :action => 'diff', - :id => @project, - :repository_id => @repository.identifier_param, - :path => "", - :rev => @changeset.identifier) if @changeset.filechanges.any? %>

    - -
    -<%= render_changeset_changes %> -
    -<% end %> - -<% content_for :header_tags do %> -<%= stylesheet_link_tag "scm" %> -<% end %> - -<% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/228ab5b14477141cc691a1ebb6e20efbefbf1ba7.svn-base --- a/.svn/pristine/22/228ab5b14477141cc691a1ebb6e20efbefbf1ba7.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ - - - - - - - - - <% for new_status in @statuses %> - - <% end %> - - - - <% for old_status in @statuses %> - "> - - <% for new_status in @statuses -%> - <% checked = workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id} %> - - <% end -%> - - <% end %> - -
    - <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')", - :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> - <%=l(:label_current_status)%> - <%=l(:label_new_statuses_allowed)%>
    - <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')", - :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> - <%=h new_status.name %> -
    - <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')", - :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> - - <%=h old_status.name %> - - <%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, checked, - :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %> -
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/229053adf4d0f0cdc30b283c6478a18c47c6fe1d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/229053adf4d0f0cdc30b283c6478a18c47c6fe1d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,161 @@ +if RUBY_VERSION < '1.9' + require 'iconv' +end + +module Redmine + module CodesetUtil + + def self.replace_invalid_utf8(str) + return str if str.nil? + if str.respond_to?(:force_encoding) + str.force_encoding('UTF-8') + if ! str.valid_encoding? + str = str.encode("US-ASCII", :invalid => :replace, + :undef => :replace, :replace => '?').encode("UTF-8") + end + elsif RUBY_PLATFORM == 'java' + begin + ic = Iconv.new('UTF-8', 'UTF-8') + str = ic.iconv(str) + rescue + str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?') + end + else + ic = Iconv.new('UTF-8', 'UTF-8') + txtar = "" + begin + txtar += ic.iconv(str) + rescue Iconv::IllegalSequence + txtar += $!.success + str = '?' + $!.failed[1,$!.failed.length] + retry + rescue + txtar += $!.success + end + str = txtar + end + str + end + + def self.to_utf8(str, encoding) + return str if str.nil? + str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) + if str.empty? + str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) + return str + end + enc = encoding.blank? ? "UTF-8" : encoding + if str.respond_to?(:force_encoding) + if enc.upcase != "UTF-8" + str.force_encoding(enc) + str = str.encode("UTF-8", :invalid => :replace, + :undef => :replace, :replace => '?') + else + str.force_encoding("UTF-8") + if ! str.valid_encoding? + str = str.encode("US-ASCII", :invalid => :replace, + :undef => :replace, :replace => '?').encode("UTF-8") + end + end + elsif RUBY_PLATFORM == 'java' + begin + ic = Iconv.new('UTF-8', enc) + str = ic.iconv(str) + rescue + str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?') + end + else + ic = Iconv.new('UTF-8', enc) + txtar = "" + begin + txtar += ic.iconv(str) + rescue Iconv::IllegalSequence + txtar += $!.success + str = '?' + $!.failed[1,$!.failed.length] + retry + rescue + txtar += $!.success + end + str = txtar + end + str + end + + def self.to_utf8_by_setting(str) + return str if str.nil? + str = self.to_utf8_by_setting_internal(str) + if str.respond_to?(:force_encoding) + str.force_encoding('UTF-8') + end + str + end + + def self.to_utf8_by_setting_internal(str) + return str if str.nil? + if str.respond_to?(:force_encoding) + str.force_encoding('ASCII-8BIT') + end + return str if str.empty? + return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii + if str.respond_to?(:force_encoding) + str.force_encoding('UTF-8') + end + encodings = Setting.repositories_encodings.split(',').collect(&:strip) + encodings.each do |encoding| + if str.respond_to?(:force_encoding) + begin + str.force_encoding(encoding) + utf8 = str.encode('UTF-8') + return utf8 if utf8.valid_encoding? + rescue + # do nothing here and try the next encoding + end + else + begin + return Iconv.conv('UTF-8', encoding, str) + rescue Iconv::Failure + # do nothing here and try the next encoding + end + end + end + str = self.replace_invalid_utf8(str) + if str.respond_to?(:force_encoding) + str.force_encoding('UTF-8') + end + str + end + + def self.from_utf8(str, encoding) + str ||= '' + if str.respond_to?(:force_encoding) + str.force_encoding('UTF-8') + if encoding.upcase != 'UTF-8' + str = str.encode(encoding, :invalid => :replace, + :undef => :replace, :replace => '?') + else + str = self.replace_invalid_utf8(str) + end + elsif RUBY_PLATFORM == 'java' + begin + ic = Iconv.new(encoding, 'UTF-8') + str = ic.iconv(str) + rescue + str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?') + end + else + ic = Iconv.new(encoding, 'UTF-8') + txtar = "" + begin + txtar += ic.iconv(str) + rescue Iconv::IllegalSequence + txtar += $!.success + str = '?' + $!.failed[1, $!.failed.length] + retry + rescue + txtar += $!.success + end + str = txtar + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/22a3e7cf343f9b15be9c525c8f86061da281f88b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22a3e7cf343f9b15be9c525c8f86061da281f88b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,260 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RepositorySubversionTest < ActiveSupport::TestCase + fixtures :projects, :repositories, :enabled_modules, :users, :roles + + include Redmine::I18n + + NUM_REV = 11 + + def setup + @project = Project.find(3) + @repository = Repository::Subversion.create(:project => @project, + :url => self.class.subversion_repository_url) + assert @repository + end + + def test_invalid_url + set_language_if_valid 'en' + ['invalid', 'http://', 'svn://', 'svn+ssh://', 'file://'].each do |url| + repo = Repository::Subversion.new( + :project => @project, + :identifier => 'test', + :url => url + ) + assert !repo.save + assert_equal ["is invalid"], repo.errors[:url] + end + end + + def test_valid_url + ['http://valid', 'svn://valid', 'svn+ssh://valid', 'file://valid'].each do |url| + repo = Repository::Subversion.new( + :project => @project, + :identifier => 'test', + :url => url + ) + assert repo.save + assert_equal [], repo.errors[:url] + assert repo.destroy + end + end + + if repository_configured?('subversion') + def test_fetch_changesets_from_scratch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + + assert_equal NUM_REV, @repository.changesets.count + assert_equal 20, @repository.filechanges.count + assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments + end + + def test_fetch_changesets_incremental + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + # Remove changesets with revision > 5 + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 5} + @project.reload + @repository.reload + assert_equal 5, @repository.changesets.count + + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + end + + def test_entries + entries = @repository.entries + assert_kind_of Redmine::Scm::Adapters::Entries, entries + end + + def test_entries_for_invalid_path_should_return_nil + entries = @repository.entries('invalid_path') + assert_nil entries + end + + def test_latest_changesets + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + # with limit + changesets = @repository.latest_changesets('', nil, 2) + assert_equal 2, changesets.size + assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets + + # with path + changesets = @repository.latest_changesets('subversion_test/folder', nil) + assert_equal ["10", "9", "7", "6", "5", "2"], changesets.collect(&:revision) + + # with path and revision + changesets = @repository.latest_changesets('subversion_test/folder', 8) + assert_equal ["7", "6", "5", "2"], changesets.collect(&:revision) + end + + def test_directory_listing_with_square_brackets_in_path + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + entries = @repository.entries('subversion_test/[folder_with_brackets]') + assert_not_nil entries, 'Expect to find entries in folder_with_brackets' + assert_equal 1, entries.size, 'Expect one entry in folder_with_brackets' + assert_equal 'README.txt', entries.first.name + end + + def test_directory_listing_with_square_brackets_in_base + @project = Project.find(3) + @repository = Repository::Subversion.create( + :project => @project, + :url => "file:///#{self.class.repository_path('subversion')}/subversion_test/[folder_with_brackets]") + + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + + assert_equal 1, @repository.changesets.count, 'Expected to see 1 revision' + assert_equal 2, @repository.filechanges.count, 'Expected to see 2 changes, dir add and file add' + + entries = @repository.entries('') + assert_not_nil entries, 'Expect to find entries' + assert_equal 1, entries.size, 'Expect a single entry' + assert_equal 'README.txt', entries.first.name + end + + def test_identifier + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + c = @repository.changesets.find_by_revision('1') + assert_equal c.revision, c.identifier + end + + def test_find_changeset_by_empty_name + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['', ' ', nil].each do |r| + assert_nil @repository.find_changeset_by_name(r) + end + end + + def test_identifier_nine_digit + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '123456789', :comments => 'test') + assert_equal c.identifier, c.revision + end + + def test_format_identifier + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + c = @repository.changesets.find_by_revision('1') + assert_equal c.format_identifier, c.revision + end + + def test_format_identifier_nine_digit + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '123456789', :comments => 'test') + assert_equal c.format_identifier, c.revision + end + + def test_activities + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '1', :comments => 'test') + assert c.event_title.include?('1:') + assert_equal '1', c.event_url[:rev] + end + + def test_activities_nine_digit + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '123456789', :comments => 'test') + assert c.event_title.include?('123456789:') + assert_equal '123456789', c.event_url[:rev] + end + + def test_log_encoding_ignore_setting + with_settings :commit_logs_encoding => 'windows-1252' do + s1 = "\xC2\x80" + s2 = "\xc3\x82\xc2\x80" + if s1.respond_to?(:force_encoding) + s1.force_encoding('ISO-8859-1') + s2.force_encoding('UTF-8') + assert_equal s1.encode('UTF-8'), s2 + end + c = Changeset.new(:repository => @repository, + :comments => s2, + :revision => '123', + :committed_on => Time.now) + assert c.save + assert_equal s2, c.comments + end + end + + def test_previous + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('3') + assert_equal @repository.find_changeset_by_name('2'), changeset.previous + end + + def test_previous_nil + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('1') + assert_nil changeset.previous + end + + def test_next + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('2') + assert_equal @repository.find_changeset_by_name('3'), changeset.next + end + + def test_next_nil + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('11') + assert_nil changeset.next + end + else + puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/22ba58471e8f0b576b63d1df7183e3dc50b2b793.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22ba58471e8f0b576b63d1df7183e3dc50b2b793.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,39 @@ +# Redmine runs tests on own continuous integration server. +# http://www.redmine.org/projects/redmine/wiki/Continuous_integration +# You can also run tests on your environment. +language: ruby +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - 2.0 + - 2.1 + - jruby +matrix: + allow_failures: + # SCM tests fail randomly due to IO.popen(). + # https://github.com/jruby/jruby/issues/779 + - rvm: jruby +env: + - "TEST_SUITE=units DATABASE_ADAPTER=postgresql" + - "TEST_SUITE=functionals DATABASE_ADAPTER=postgresql" + - "TEST_SUITE=integration DATABASE_ADAPTER=postgresql" + - "TEST_SUITE=units DATABASE_ADAPTER=mysql" + - "TEST_SUITE=functionals DATABASE_ADAPTER=mysql" + - "TEST_SUITE=integration DATABASE_ADAPTER=mysql" + - "TEST_SUITE=units DATABASE_ADAPTER=sqlite3" + - "TEST_SUITE=functionals DATABASE_ADAPTER=sqlite3" + - "TEST_SUITE=integration DATABASE_ADAPTER=sqlite3" +before_install: + - "sudo apt-get update -qq" + - "sudo apt-get --no-install-recommends install bzr cvs git mercurial subversion" +script: + - "SCMS=bazaar,cvs,subversion,git,mercurial,filesystem" + - "export SCMS" + - "git --version" + - "bundle install" + - "RUN_ON_NOT_OFFICIAL='' RUBY_VER=1.9 BRANCH=trunk bundle exec rake config/database.yml" + - "bundle install" + - "JRUBY_OPTS=-J-Xmx1024m bundle exec rake ci" +notifications: + email: false diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/22d0c7f9d45361c16b2b7eccd0c488f009ccf152.svn-base --- a/.svn/pristine/22/22d0c7f9d45361c16b2b7eccd0c488f009ccf152.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 TimeEntryActivityTest < ActiveSupport::TestCase - fixtures :enumerations, :time_entries, :custom_fields - - include Redmine::I18n - - def test_should_be_an_enumeration - assert TimeEntryActivity.ancestors.include?(Enumeration) - end - - def test_objects_count - assert_equal 3, TimeEntryActivity.find_by_name("Design").objects_count - assert_equal 2, TimeEntryActivity.find_by_name("Development").objects_count - end - - def test_option_name - assert_equal :enumeration_activities, TimeEntryActivity.new.option_name - end - - def test_create_with_custom_field - field = TimeEntryActivityCustomField.find_by_name('Billable') - e = TimeEntryActivity.new(:name => 'Custom Data') - e.custom_field_values = {field.id => "1"} - assert e.save - - e.reload - assert_equal "1", e.custom_value_for(field).value - end - - def test_create_without_required_custom_field_should_fail - set_language_if_valid 'en' - field = TimeEntryActivityCustomField.find_by_name('Billable') - field.update_attribute(:is_required, true) - - e = TimeEntryActivity.new(:name => 'Custom Data') - assert !e.save - assert_equal ["Billable can't be blank"], e.errors.full_messages - end - - def test_create_with_required_custom_field_should_succeed - field = TimeEntryActivityCustomField.find_by_name('Billable') - field.update_attribute(:is_required, true) - - e = TimeEntryActivity.new(:name => 'Custom Data') - e.custom_field_values = {field.id => "1"} - assert e.save - end - - def test_update_with_required_custom_field_change - set_language_if_valid 'en' - field = TimeEntryActivityCustomField.find_by_name('Billable') - field.update_attribute(:is_required, true) - - e = TimeEntryActivity.find(10) - assert e.available_custom_fields.include?(field) - # No change to custom field, record can be saved - assert e.save - # Blanking custom field, save should fail - e.custom_field_values = {field.id => ""} - assert !e.save - assert_equal ["Billable can't be blank"], e.errors.full_messages - - # Update custom field to valid value, save should succeed - e.custom_field_values = {field.id => "0"} - assert e.save - e.reload - assert_equal "0", e.custom_value_for(field).value - end -end - diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/22ddddc8c878fc3e93d3136a28528f9e4f69f7e0.svn-base --- a/.svn/pristine/22/22ddddc8c878fc3e93d3136a28528f9e4f69f7e0.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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::SafeAttributesTest < ActiveSupport::TestCase - fixtures :users - - class Base - def attributes=(attrs) - attrs.each do |key, value| - send("#{key}=", value) - end - end - end - - class Person < Base - attr_accessor :firstname, :lastname, :login - include Redmine::SafeAttributes - safe_attributes :firstname, :lastname - safe_attributes :login, :if => lambda {|person, user| user.admin?} - end - - class Book < Base - attr_accessor :title - include Redmine::SafeAttributes - safe_attributes :title - end - - def test_safe_attribute_names - p = Person.new - user = User.anonymous - assert_equal ['firstname', 'lastname'], p.safe_attribute_names(user) - assert p.safe_attribute?('firstname', user) - assert !p.safe_attribute?('login', user) - - p = Person.new - user = User.find(1) - assert_equal ['firstname', 'lastname', 'login'], p.safe_attribute_names(user) - assert p.safe_attribute?('firstname', user) - assert p.safe_attribute?('login', user) - end - - def test_safe_attribute_names_without_user - p = Person.new - User.current = nil - assert_equal ['firstname', 'lastname'], p.safe_attribute_names - assert p.safe_attribute?('firstname') - assert !p.safe_attribute?('login') - - p = Person.new - User.current = User.find(1) - assert_equal ['firstname', 'lastname', 'login'], p.safe_attribute_names - assert p.safe_attribute?('firstname') - assert p.safe_attribute?('login') - end - - def test_set_safe_attributes - p = Person.new - p.send('safe_attributes=', {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'}, User.anonymous) - assert_equal 'John', p.firstname - assert_equal 'Smith', p.lastname - assert_nil p.login - - p = Person.new - User.current = User.find(1) - p.send('safe_attributes=', {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'}, User.find(1)) - assert_equal 'John', p.firstname - assert_equal 'Smith', p.lastname - assert_equal 'jsmith', p.login - end - - def test_set_safe_attributes_without_user - p = Person.new - User.current = nil - p.safe_attributes = {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'} - assert_equal 'John', p.firstname - assert_equal 'Smith', p.lastname - assert_nil p.login - - p = Person.new - User.current = User.find(1) - p.safe_attributes = {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'} - assert_equal 'John', p.firstname - assert_equal 'Smith', p.lastname - assert_equal 'jsmith', p.login - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/22/22e0815b30d651aac0c03e283ee06938ada8af0c.svn-base --- a/.svn/pristine/22/22e0815b30d651aac0c03e283ee06938ada8af0c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'net/imap' - -module Redmine - module IMAP - class << self - def check(imap_options={}, options={}) - host = imap_options[:host] || '127.0.0.1' - port = imap_options[:port] || '143' - ssl = !imap_options[:ssl].nil? - folder = imap_options[:folder] || 'INBOX' - - imap = Net::IMAP.new(host, port, ssl) - imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil? - imap.select(folder) - imap.search(['NOT', 'SEEN']).each do |message_id| - msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822'] - logger.debug "Receiving message #{message_id}" if logger && logger.debug? - if MailHandler.receive(msg, options) - logger.debug "Message #{message_id} successfully received" if logger && logger.debug? - if imap_options[:move_on_success] - imap.copy(message_id, imap_options[:move_on_success]) - end - imap.store(message_id, "+FLAGS", [:Seen, :Deleted]) - else - logger.debug "Message #{message_id} can not be processed" if logger && logger.debug? - imap.store(message_id, "+FLAGS", [:Seen]) - if imap_options[:move_on_failure] - imap.copy(message_id, imap_options[:move_on_failure]) - imap.store(message_id, "+FLAGS", [:Deleted]) - end - end - end - imap.expunge - end - - private - - def logger - ::Rails.logger - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/23/23161378fd0429fed8a9ad8cd7fdacb71cc2db68.svn-base --- a/.svn/pristine/23/23161378fd0429fed8a9ad8cd7fdacb71cc2db68.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -<%= error_messages_for @group %> - -
    -

    <%= f.text_field :name %>

    - <% @group.custom_field_values.each do |value| %> -

    <%= custom_field_tag_with_label :group, value %>

    - <% end %> -
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/23/231b70e58d27161a5f2deb16ab2e057f571aa2ab.svn-base --- a/.svn/pristine/23/231b70e58d27161a5f2deb16ab2e057f571aa2ab.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'blankslate' - -module Redmine - module Views - module Builders - class Json < Structure - attr_accessor :jsonp - - def initialize(request, response) - super - self.jsonp = (request.params[:callback] || request.params[:jsonp]).to_s.gsub(/[^a-zA-Z0-9_]/, '') - end - - def output - json = @struct.first.to_json - if jsonp.present? - json = "#{jsonp}(#{json})" - response.content_type = 'application/javascript' - end - json - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/23/2334e365cbae3c0f02226259643ce7e569ce00f6.svn-base --- a/.svn/pristine/23/2334e365cbae3c0f02226259643ce7e569ce00f6.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CommentsControllerTest < ActionController::TestCase - fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments - - def setup - User.current = nil - end - - def test_add_comment - @request.session[:user_id] = 2 - post :create, :id => 1, :comment => { :comments => 'This is a test comment' } - assert_redirected_to '/news/1' - - comment = News.find(1).comments.last - assert_not_nil comment - assert_equal 'This is a test comment', comment.comments - assert_equal User.find(2), comment.author - end - - def test_empty_comment_should_not_be_added - @request.session[:user_id] = 2 - assert_no_difference 'Comment.count' do - post :create, :id => 1, :comment => { :comments => '' } - assert_response :redirect - assert_redirected_to '/news/1' - end - end - - def test_create_should_be_denied_if_news_is_not_commentable - News.any_instance.stubs(:commentable?).returns(false) - @request.session[:user_id] = 2 - assert_no_difference 'Comment.count' do - post :create, :id => 1, :comment => { :comments => 'This is a test comment' } - assert_response 403 - end - end - - def test_destroy_comment - comments_count = News.find(1).comments.size - @request.session[:user_id] = 2 - delete :destroy, :id => 1, :comment_id => 2 - assert_redirected_to '/news/1' - assert_nil Comment.find_by_id(2) - assert_equal comments_count - 1, News.find(1).comments.size - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/23/23389be83ae1e1f9aef946f48e6c0e1c8c6bbfb1.svn-base --- a/.svn/pristine/23/23389be83ae1e1f9aef946f48e6c0e1c8c6bbfb1.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,263 +0,0 @@ -<% @gantt.view = self %> -

    <%= @query.new_record? ? l(:label_gantt) : h(@query.name) %>

    - -<%= form_tag({:controller => 'gantts', :action => 'show', - :project_id => @project, :month => params[:month], - :year => params[:year], :months => params[:months]}, - :method => :get, :id => 'query_form') do %> -<%= hidden_field_tag 'set_filter', '1' %> -
    "> - <%= l(:label_filter_plural) %> -
    "> - <%= render :partial => 'queries/filters', :locals => {:query => @query} %> -
    -
    - -

    - <%= gantt_zoom_link(@gantt, :in) %> - <%= gantt_zoom_link(@gantt, :out) %> -

    - -

    -<%= text_field_tag 'months', @gantt.months, :size => 2 %> -<%= l(:label_months_from) %> -<%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %> -<%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %> -<%= hidden_field_tag 'zoom', @gantt.zoom %> - -<%= link_to_function l(:button_apply), '$("#query_form").submit()', - :class => 'icon icon-checked' %> -<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, - :class => 'icon icon-reload' %> -

    -<% end %> - -<%= error_messages_for 'query' %> -<% if @query.valid? %> -<% - zoom = 1 - @gantt.zoom.times { zoom = zoom * 2 } - - subject_width = 330 - header_heigth = 18 - - headers_height = header_heigth - show_weeks = false - show_days = false - - if @gantt.zoom > 1 - show_weeks = true - headers_height = 2 * header_heigth - if @gantt.zoom > 2 - show_days = true - headers_height = 3 * header_heigth - end - end - - # Width of the entire chart - g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i - @gantt.render(:top => headers_height + 8, - :zoom => zoom, - :g_width => g_width, - :subject_width => subject_width) - g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max - t_height = g_height + headers_height -%> - -<% if @gantt.truncated %> -

    <%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %>

    -<% end %> - - - - - - - -
    - <% - style = "" - style += "position:relative;" - style += "height: #{t_height + 24}px;" - style += "width: #{subject_width + 1}px;" - %> - <%= content_tag(:div, :style => style) do %> - <% - style = "" - style += "right:-2px;" - style += "width: #{subject_width}px;" - style += "height: #{headers_height}px;" - style += 'background: #eee;' - %> - <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> - <% - style = "" - style += "right:-2px;" - style += "width: #{subject_width}px;" - style += "height: #{t_height}px;" - style += 'border-left: 1px solid #c0c0c0;' - style += 'overflow: hidden;' - %> - <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> - <%= content_tag(:div, :class => "gantt_subjects") do %> - <%= @gantt.subjects.html_safe %> - <% end %> - <% end %> - -
    -<% - style = "" - style += "width: #{g_width - 1}px;" - style += "height: #{headers_height}px;" - style += 'background: #eee;' -%> -<%= content_tag(:div, ' '.html_safe, :style => style, :class => "gantt_hdr") %> - -<% ###### Months headers ###### %> -<% - month_f = @gantt.date_from - left = 0 - height = (show_weeks ? header_heigth : header_heigth + g_height) -%> -<% @gantt.months.times do %> - <% - width = (((month_f >> 1) - month_f) * zoom - 1).to_i - style = "" - style += "left: #{left}px;" - style += "width: #{width}px;" - style += "height: #{height}px;" - %> - <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %> - <%= link_to h("#{month_f.year}-#{month_f.month}"), - @gantt.params.merge(:year => month_f.year, :month => month_f.month), - :title => "#{month_name(month_f.month)} #{month_f.year}" %> - <% end %> - <% - left = left + width + 1 - month_f = month_f >> 1 - %> -<% end %> - -<% ###### Weeks headers ###### %> -<% if show_weeks %> - <% - left = 0 - height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height) - %> - <% if @gantt.date_from.cwday == 1 %> - <% - # @date_from is monday - week_f = @gantt.date_from - %> - <% else %> - <% - # find next monday after @date_from - week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1) - width = (7 - @gantt.date_from.cwday + 1) * zoom - 1 - style = "" - style += "left: #{left}px;" - style += "top: 19px;" - style += "width: #{width}px;" - style += "height: #{height}px;" - %> - <%= content_tag(:div, ' '.html_safe, - :style => style, :class => "gantt_hdr") %> - <% left = left + width + 1 %> - <% end %> - <% while week_f <= @gantt.date_to %> - <% - width = ((week_f + 6 <= @gantt.date_to) ? - 7 * zoom - 1 : - (@gantt.date_to - week_f + 1) * zoom - 1).to_i - style = "" - style += "left: #{left}px;" - style += "top: 19px;" - style += "width: #{width}px;" - style += "height: #{height}px;" - %> - <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %> - <%= content_tag(:small) do %> - <%= week_f.cweek if width >= 16 %> - <% end %> - <% end %> - <% - left = left + width + 1 - week_f = week_f + 7 - %> - <% end %> -<% end %> - -<% ###### Days headers ####### %> -<% if show_days %> - <% - left = 0 - height = g_height + header_heigth - 1 - wday = @gantt.date_from.cwday - %> - <% (@gantt.date_to - @gantt.date_from + 1).to_i.times do %> - <% - width = zoom - 1 - style = "" - style += "left: #{left}px;" - style += "top:37px;" - style += "width: #{width}px;" - style += "height: #{height}px;" - style += "font-size:0.7em;" - clss = "gantt_hdr" - clss << " nwday" if @gantt.non_working_week_days.include?(wday) - %> - <%= content_tag(:div, :style => style, :class => clss) do %> - <%= day_letter(wday) %> - <% end %> - <% - left = left + width + 1 - wday = wday + 1 - wday = 1 if wday > 7 - %> - <% end %> -<% end %> - -<%= @gantt.lines.html_safe %> - -<% ###### Today red line (excluded from cache) ###### %> -<% if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %> - <% - today_left = (((Date.today - @gantt.date_from + 1) * zoom).floor() - 1).to_i - style = "" - style += "position: absolute;" - style += "height: #{g_height}px;" - style += "top: #{headers_height + 1}px;" - style += "left: #{today_left}px;" - style += "width:10px;" - style += "border-left: 1px dashed red;" - %> - <%= content_tag(:div, ' '.html_safe, :style => style) %> -<% end %> - -
    -
    - - - - - - -
    - <%= link_to_content_update("\xc2\xab " + l(:label_previous), - params.merge(@gantt.params_previous)) %> - - <%= link_to_content_update(l(:label_next) + " \xc2\xbb", - params.merge(@gantt.params_next)) %> -
    - -<% other_formats_links do |f| %> - <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %> - <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %> -<% end %> -<% end # query.valid? %> - -<% content_for :sidebar do %> - <%= render :partial => 'issues/sidebar' %> -<% end %> - -<% html_title(l(:label_gantt)) -%> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/23/23f0efc94d28d36505686b37f2fa44ea0d643245.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/23/23f0efc94d28d36505686b37f2fa44ea0d643245.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1122 @@ +# Indonesian translations +# by Raden Prabowo (cakbowo@gmail.com) + +id: + direction: ltr + date: + formats: + default: "%d-%m-%Y" + short: "%d %b" + long: "%d %B %Y" + + day_names: [Minggu, Senin, Selasa, Rabu, Kamis, Jumat, Sabtu] + abbr_day_names: [Ming, Sen, Sel, Rab, Kam, Jum, Sab] + + month_names: [~, Januari, Februari, Maret, April, Mei, Juni, Juli, Agustus, September, Oktober, November, Desember] + abbr_month_names: [~, Jan, Feb, Mar, Apr, Mei, Jun, Jul, Agu, Sep, Okt, Nov, Des] + order: + - :day + - :month + - :year + + time: + formats: + default: "%a %d %b %Y, %H:%M:%S" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%d %B %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "setengah menit" + less_than_x_seconds: + one: "kurang dari sedetik" + other: "kurang dari %{count} detik" + x_seconds: + one: "sedetik" + other: "%{count} detik" + less_than_x_minutes: + one: "kurang dari semenit" + other: "kurang dari %{count} menit" + x_minutes: + one: "semenit" + other: "%{count} menit" + about_x_hours: + one: "sekitar sejam" + other: "sekitar %{count} jam" + x_hours: + one: "1 jam" + other: "%{count} jam" + x_days: + one: "sehari" + other: "%{count} hari" + about_x_months: + one: "sekitar sebulan" + other: "sekitar %{count} bulan" + x_months: + one: "sebulan" + other: "%{count} bulan" + about_x_years: + one: "sekitar setahun" + other: "sekitar %{count} tahun" + over_x_years: + one: "lebih dari setahun" + other: "lebih dari %{count} tahun" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + format: + precision: 3 + separator: ',' + delimiter: '.' + currency: + format: + unit: 'Rp' + precision: 2 + format: '%n %u' + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: + sentence_connector: "dan" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "tidak termasuk dalam daftar" + exclusion: "sudah dicadangkan" + invalid: "salah" + confirmation: "tidak sesuai konfirmasi" + accepted: "harus disetujui" + empty: "tidak boleh kosong" + blank: "tidak boleh kosong" + too_long: "terlalu panjang (maksimum %{count} karakter)" + too_short: "terlalu pendek (minimum %{count} karakter)" + wrong_length: "panjangnya salah (seharusnya %{count} karakter)" + taken: "sudah diambil" + not_a_number: "bukan angka" + not_a_date: "bukan tanggal" + greater_than: "harus lebih besar dari %{count}" + greater_than_or_equal_to: "harus lebih besar atau sama dengan %{count}" + equal_to: "harus sama dengan %{count}" + less_than: "harus kurang dari %{count}" + less_than_or_equal_to: "harus kurang atau sama dengan %{count}" + odd: "harus ganjil" + even: "harus genap" + greater_than_start_date: "harus lebih besar dari tanggal mulai" + not_same_project: "tidak tergabung dalam proyek yang sama" + circular_dependency: "kaitan ini akan menghasilkan circular dependency" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Silakan pilih + + general_text_No: 'Tidak' + general_text_Yes: 'Ya' + general_text_no: 'tidak' + general_text_yes: 'ya' + general_lang_name: 'Indonesia' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-1 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_updated: Akun sudah berhasil diperbarui. + notice_account_invalid_creditentials: Pengguna atau kata sandi salah + notice_account_password_updated: Kata sandi sudah berhasil diperbarui. + notice_account_wrong_password: Kata sandi salah. + notice_account_register_done: Akun sudah berhasil dibuat. Untuk mengaktifkan akun anda, silakan klik tautan (link) yang dikirim kepada anda melalui e-mail. + notice_account_unknown_email: Pengguna tidak dikenal. + notice_can_t_change_password: Akun ini menggunakan sumber otentikasi eksternal yang tidak dikenal. Kata sandi tidak bisa diubah. + notice_account_lost_email_sent: Email berisi instruksi untuk memilih kata sandi baru sudah dikirimkan kepada anda. + notice_account_activated: Akun anda sudah diaktifasi. Sekarang anda bisa login. + notice_successful_create: Berhasil dibuat. + notice_successful_update: Berhasil diperbarui. + notice_successful_delete: Berhasil dihapus. + notice_successful_connection: Berhasil terhubung. + notice_file_not_found: Berkas yang anda buka tidak ada atau sudah dihapus. + notice_locking_conflict: Data sudah diubah oleh pengguna lain. + notice_not_authorized: Anda tidak memiliki akses ke halaman ini. + notice_email_sent: "Email sudah dikirim ke %{value}" + notice_email_error: "Terjadi kesalahan pada saat pengiriman email (%{value})" + notice_feeds_access_key_reseted: Atom access key anda sudah direset. + notice_failed_to_save_issues: "Gagal menyimpan %{count} masalah dari %{total} yang dipilih: %{ids}." + notice_no_issue_selected: "Tidak ada masalah yang dipilih! Silakan pilih masalah yang akan anda sunting." + notice_account_pending: "Akun anda sudah dibuat dan sekarang sedang menunggu persetujuan administrator." + notice_default_data_loaded: Konfigurasi default sudah berhasil dimuat. + notice_unable_delete_version: Tidak bisa menghapus versi. + + error_can_t_load_default_data: "Konfigurasi default tidak bisa dimuat: %{value}" + error_scm_not_found: "Entri atau revisi tidak terdapat pada repositori." + error_scm_command_failed: "Terjadi kesalahan pada saat mengakses repositori: %{value}" + error_scm_annotate: "Entri tidak ada, atau tidak dapat di anotasi." + error_issue_not_found_in_project: 'Masalah tidak ada atau tidak tergabung dalam proyek ini.' + error_no_tracker_in_project: 'Tidak ada pelacak yang diasosiasikan pada proyek ini. Silakan pilih Pengaturan Proyek.' + error_no_default_issue_status: 'Nilai default untuk Status masalah belum didefinisikan. Periksa kembali konfigurasi anda (Pilih "Administrasi --> Status masalah").' + error_can_not_reopen_issue_on_closed_version: 'Masalah yang ditujukan pada versi tertutup tidak bisa dibuka kembali' + error_can_not_archive_project: Proyek ini tidak bisa diarsipkan + + warning_attachments_not_saved: "%{count} berkas tidak bisa disimpan." + + mail_subject_lost_password: "Kata sandi %{value} anda" + mail_body_lost_password: 'Untuk mengubah kata sandi anda, klik tautan berikut::' + mail_subject_register: "Aktivasi akun %{value} anda" + mail_body_register: 'Untuk mengaktifkan akun anda, klik tautan berikut:' + mail_body_account_information_external: "Anda dapat menggunakan akun %{value} anda untuk login." + mail_body_account_information: Informasi akun anda + mail_subject_account_activation_request: "Permintaan aktivasi akun %{value} " + mail_body_account_activation_request: "Pengguna baru (%{value}) sudan didaftarkan. Akun tersebut menunggu persetujuan anda:" + mail_subject_reminder: "%{count} masalah harus selesai pada hari berikutnya (%{days})" + mail_body_reminder: "%{count} masalah yang ditugaskan pada anda harus selesai dalam %{days} hari kedepan:" + mail_subject_wiki_content_added: "'%{id}' halaman wiki sudah ditambahkan" + mail_body_wiki_content_added: "The '%{id}' halaman wiki sudah ditambahkan oleh %{author}." + mail_subject_wiki_content_updated: "'%{id}' halaman wiki sudah diperbarui" + mail_body_wiki_content_updated: "The '%{id}' halaman wiki sudah diperbarui oleh %{author}." + + + field_name: Nama + field_description: Deskripsi + field_summary: Ringkasan + field_is_required: Dibutuhkan + field_firstname: Nama depan + field_lastname: Nama belakang + field_mail: Email + field_filename: Berkas + field_filesize: Ukuran + field_downloads: Unduhan + field_author: Pengarang + field_created_on: Dibuat + field_updated_on: Diperbarui + field_field_format: Format + field_is_for_all: Untuk semua proyek + field_possible_values: Nilai yang mungkin + field_regexp: Regular expression + field_min_length: Panjang minimum + field_max_length: Panjang maksimum + field_value: Nilai + field_category: Kategori + field_title: Judul + field_project: Proyek + field_issue: Masalah + field_status: Status + field_notes: Catatan + field_is_closed: Masalah ditutup + field_is_default: Nilai default + field_tracker: Pelacak + field_subject: Perihal + field_due_date: Harus selesai + field_assigned_to: Ditugaskan ke + field_priority: Prioritas + field_fixed_version: Versi target + field_user: Pengguna + field_role: Peran + field_homepage: Halaman web + field_is_public: Publik + field_parent: Subproyek dari + field_is_in_roadmap: Masalah ditampilkan di rencana kerja + field_login: Login + field_mail_notification: Notifikasi email + field_admin: Administrator + field_last_login_on: Terakhir login + field_language: Bahasa + field_effective_date: Tanggal + field_password: Kata sandi + field_new_password: Kata sandi baru + field_password_confirmation: Konfirmasi + field_version: Versi + field_type: Tipe + field_host: Host + field_port: Port + field_account: Akun + field_base_dn: Base DN + field_attr_login: Atribut login + field_attr_firstname: Atribut nama depan + field_attr_lastname: Atribut nama belakang + field_attr_mail: Atribut email + field_onthefly: Pembuatan pengguna seketika + field_start_date: Mulai + field_done_ratio: "% Selesai" + field_auth_source: Mode otentikasi + field_hide_mail: Sembunyikan email saya + field_comments: Komentar + field_url: URL + field_start_page: Halaman awal + field_subproject: Subproyek + field_hours: Jam + field_activity: Kegiatan + field_spent_on: Tanggal + field_identifier: Pengenal + field_is_filter: Digunakan sebagai penyaring + field_issue_to: Masalah terkait + field_delay: Tertunday + field_assignable: Masalah dapat ditugaskan pada peran ini + field_redirect_existing_links: Alihkan tautan yang ada + field_estimated_hours: Perkiraan waktu + field_column_names: Kolom + field_time_zone: Zona waktu + field_searchable: Dapat dicari + field_default_value: Nilai default + field_comments_sorting: Tampilkan komentar + field_parent_title: Halaman induk + field_editable: Dapat disunting + field_watcher: Pemantau + field_identity_url: OpenID URL + field_content: Isi + field_group_by: Dikelompokkan berdasar + field_sharing: Berbagi + + setting_app_title: Judul aplikasi + setting_app_subtitle: Subjudul aplikasi + setting_welcome_text: Teks sambutan + setting_default_language: Bahasa Default + setting_login_required: Butuhkan otentikasi + setting_self_registration: Swa-pendaftaran + setting_attachment_max_size: Ukuran maksimum untuk lampiran + setting_issues_export_limit: Batasan ukuran export masalah + setting_mail_from: Emisi alamat email + setting_bcc_recipients: Blind carbon copy recipients (bcc) + setting_plain_text_mail: Plain text mail (no HTML) + setting_host_name: Nama host dan path + setting_text_formatting: Format teks + setting_wiki_compression: Kompresi untuk riwayat wiki + setting_feeds_limit: Batasan isi feed + setting_default_projects_public: Proyek baru defaultnya adalah publik + setting_autofetch_changesets: Autofetch commits + setting_sys_api_enabled: Aktifkan WS untuk pengaturan repositori + setting_commit_ref_keywords: Referensi kaca kunci + setting_commit_fix_keywords: Pembetulan kaca kunci + setting_autologin: Autologin + setting_date_format: Format tanggal + setting_time_format: Format waktu + setting_cross_project_issue_relations: Perbolehkan kaitan masalah proyek berbeda + setting_issue_list_default_columns: Kolom default ditampilkan di daftar masalah + setting_emails_footer: Footer untuk email + setting_protocol: Protokol + setting_per_page_options: Pilihan obyek per halaman + setting_user_format: Format tampilan untuk pengguna + setting_activity_days_default: Hari tertampil pada kegiatan proyek + setting_display_subprojects_issues: Secara default, tampilkan masalah subproyek pada proyek utama + setting_enabled_scm: Enabled SCM + setting_mail_handler_api_enabled: Enable WS for incoming emails + setting_mail_handler_api_key: API key + setting_sequential_project_identifiers: Buat pengenal proyek terurut + setting_gravatar_enabled: Gunakan icon pengguna dari Gravatar + setting_gravatar_default: Gambar default untuk Gravatar + setting_diff_max_lines_displayed: Maksimum perbedaan baris tertampil + setting_file_max_size_displayed: Maksimum berkas tertampil secara inline + setting_repository_log_display_limit: Nilai maksimum dari revisi ditampilkan di log berkas + setting_openid: Perbolehkan Login dan pendaftaran melalui OpenID + setting_password_min_length: Panjang minimum untuk kata sandi + setting_new_project_user_role_id: Peran diberikan pada pengguna non-admin yang membuat proyek + setting_default_projects_modules: Modul yang diaktifkan pada proyek baru + + permission_add_project: Tambahkan proyek + permission_edit_project: Sunting proyek + permission_select_project_modules: Pilih modul proyek + permission_manage_members: Atur anggota + permission_manage_versions: Atur versi + permission_manage_categories: Atur kategori masalah + permission_add_issues: Tambahkan masalah + permission_edit_issues: Sunting masalah + permission_manage_issue_relations: Atur kaitan masalah + permission_add_issue_notes: Tambahkan catatan + permission_edit_issue_notes: Sunting catatan + permission_edit_own_issue_notes: Sunting catatan saya + permission_move_issues: Pindahkan masalah + permission_delete_issues: Hapus masalah + permission_manage_public_queries: Atur query publik + permission_save_queries: Simpan query + permission_view_gantt: Tampilkan gantt chart + permission_view_calendar: Tampilkan kalender + permission_view_issue_watchers: Tampilkan daftar pemantau + permission_add_issue_watchers: Tambahkan pemantau + permission_delete_issue_watchers: Hapus pemantau + permission_log_time: Log waktu terpakai + permission_view_time_entries: Tampilkan waktu terpakai + permission_edit_time_entries: Sunting catatan waktu + permission_edit_own_time_entries: Sunting catatan waktu saya + permission_manage_news: Atur berita + permission_comment_news: Komentari berita + permission_view_documents: Tampilkan dokumen + permission_manage_files: Atur berkas + permission_view_files: Tampilkan berkas + permission_manage_wiki: Atur wiki + permission_rename_wiki_pages: Ganti nama halaman wiki + permission_delete_wiki_pages: Hapus halaman wiki + permission_view_wiki_pages: Tampilkan wiki + permission_view_wiki_edits: Tampilkan riwayat wiki + permission_edit_wiki_pages: Sunting halaman wiki + permission_delete_wiki_pages_attachments: Hapus lampiran + permission_protect_wiki_pages: Proteksi halaman wiki + permission_manage_repository: Atur repositori + permission_browse_repository: Jelajah repositori + permission_view_changesets: Tampilkan set perubahan + permission_commit_access: Commit akses + permission_manage_boards: Atur forum + permission_view_messages: Tampilkan pesan + permission_add_messages: Tambahkan pesan + permission_edit_messages: Sunting pesan + permission_edit_own_messages: Sunting pesan saya + permission_delete_messages: Hapus pesan + permission_delete_own_messages: Hapus pesan saya + + project_module_issue_tracking: Pelacak masalah + project_module_time_tracking: Pelacak waktu + project_module_news: Berita + project_module_documents: Dokumen + project_module_files: Berkas + project_module_wiki: Wiki + project_module_repository: Repositori + project_module_boards: Forum + + label_user: Pengguna + label_user_plural: Pengguna + label_user_new: Pengguna baru + label_user_anonymous: Anonymous + label_project: Proyek + label_project_new: Proyek baru + label_project_plural: Proyek + label_x_projects: + zero: tidak ada proyek + one: 1 proyek + other: "%{count} proyek" + label_project_all: Semua Proyek + label_project_latest: Proyek terakhir + label_issue: Masalah + label_issue_new: Masalah baru + label_issue_plural: Masalah + label_issue_view_all: tampilkan semua masalah + label_issues_by: "Masalah ditambahkan oleh %{value}" + label_issue_added: Masalah ditambahan + label_issue_updated: Masalah diperbarui + label_document: Dokumen + label_document_new: Dokumen baru + label_document_plural: Dokumen + label_document_added: Dokumen ditambahkan + label_role: Peran + label_role_plural: Peran + label_role_new: Peran baru + label_role_and_permissions: Peran dan perijinan + label_member: Anggota + label_member_new: Anggota baru + label_member_plural: Anggota + label_tracker: Pelacak + label_tracker_plural: Pelacak + label_tracker_new: Pelacak baru + label_workflow: Alur kerja + label_issue_status: Status masalah + label_issue_status_plural: Status masalah + label_issue_status_new: Status baru + label_issue_category: Kategori masalah + label_issue_category_plural: Kategori masalah + label_issue_category_new: Kategori baru + label_custom_field: Field kustom + label_custom_field_plural: Field kustom + label_custom_field_new: Field kustom + label_enumerations: Enumerasi + label_enumeration_new: Buat baru + label_information: Informasi + label_information_plural: Informasi + label_please_login: Silakan login + label_register: mendaftar + label_login_with_open_id_option: atau login menggunakan OpenID + label_password_lost: Lupa password + label_home: Halaman depan + label_my_page: Beranda + label_my_account: Akun saya + label_my_projects: Proyek saya + label_administration: Administrasi + label_login: Login + label_logout: Keluar + label_help: Bantuan + label_reported_issues: Masalah terlapor + label_assigned_to_me_issues: Masalah yang ditugaskan pada saya + label_last_login: Terakhir login + label_registered_on: Terdaftar pada + label_activity: Kegiatan + label_overall_activity: Kegiatan umum + label_user_activity: "kegiatan %{value}" + label_new: Baru + label_logged_as: Login sebagai + label_environment: Lingkungan + label_authentication: Otentikasi + label_auth_source: Mode Otentikasi + label_auth_source_new: Mode otentikasi baru + label_auth_source_plural: Mode Otentikasi + label_subproject_plural: Subproyek + label_and_its_subprojects: "%{value} dan subproyeknya" + label_min_max_length: Panjang Min - Maks + label_list: Daftar + label_date: Tanggal + label_integer: Integer + label_float: Float + label_boolean: Boolean + label_string: Text + label_text: Long text + label_attribute: Atribut + label_attribute_plural: Atribut + label_no_data: Tidak ada data untuk ditampilkan + label_change_status: Status perubahan + label_history: Riwayat + label_attachment: Berkas + label_attachment_new: Berkas baru + label_attachment_delete: Hapus Berkas + label_attachment_plural: Berkas + label_file_added: Berkas ditambahkan + label_report: Laporan + label_report_plural: Laporan + label_news: Berita + label_news_new: Tambahkan berita + label_news_plural: Berita + label_news_latest: Berita terakhir + label_news_view_all: Tampilkan semua berita + label_news_added: Berita ditambahkan + label_settings: Pengaturan + label_overview: Umum + label_version: Versi + label_version_new: Versi baru + label_version_plural: Versi + label_confirmation: Konfirmasi + label_export_to: 'Juga tersedia dalam:' + label_read: Baca... + label_public_projects: Proyek publik + label_open_issues: belum selesai + label_open_issues_plural: belum selesai + label_closed_issues: selesai + label_closed_issues_plural: selesai + label_x_open_issues_abbr_on_total: + zero: 0 belum selesai / %{total} + one: 1 belum selesai / %{total} + other: "%{count} terbuka / %{total}" + label_x_open_issues_abbr: + zero: 0 belum selesai + one: 1 belum selesai + other: "%{count} belum selesai" + label_x_closed_issues_abbr: + zero: 0 selesai + one: 1 selesai + other: "%{count} selesai" + label_total: Total + label_permissions: Perijinan + label_current_status: Status sekarang + label_new_statuses_allowed: Status baru yang diijinkan + label_all: semua + label_none: tidak ada + label_nobody: tidak ada + label_next: Berikut + label_previous: Sebelum + label_used_by: Digunakan oleh + label_details: Rincian + label_add_note: Tambahkan catatan + label_per_page: Per halaman + label_calendar: Kalender + label_months_from: dari bulan + label_gantt: Gantt + label_internal: Internal + label_last_changes: "%{count} perubahan terakhir" + label_change_view_all: Tampilkan semua perubahan + label_personalize_page: Personalkan halaman ini + label_comment: Komentar + label_comment_plural: Komentar + label_x_comments: + zero: tak ada komentar + one: 1 komentar + other: "%{count} komentar" + label_comment_add: Tambahkan komentar + label_comment_added: Komentar ditambahkan + label_comment_delete: Hapus komentar + label_query: Custom query + label_query_plural: Custom queries + label_query_new: Query baru + label_filter_add: Tambahkan filter + label_filter_plural: Filter + label_equals: sama dengan + label_not_equals: tidak sama dengan + label_in_less_than: kurang dari + label_in_more_than: lebih dari + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: pada + label_today: hari ini + label_all_time: semua waktu + label_yesterday: kemarin + label_this_week: minggu ini + label_last_week: minggu lalu + label_last_n_days: "%{count} hari terakhir" + label_this_month: bulan ini + label_last_month: bulan lalu + label_this_year: this year + label_date_range: Jangkauan tanggal + label_less_than_ago: kurang dari hari yang lalu + label_more_than_ago: lebih dari hari yang lalu + label_ago: hari yang lalu + label_contains: berisi + label_not_contains: tidak berisi + label_day_plural: hari + label_repository: Repositori + label_repository_plural: Repositori + label_browse: Jelajah + label_branch: Cabang + label_tag: Tag + label_revision: Revisi + label_revision_plural: Revisi + label_associated_revisions: Revisi terkait + label_added: ditambahkan + label_modified: diubah + label_copied: disalin + label_renamed: diganti nama + label_deleted: dihapus + label_latest_revision: Revisi terakhir + label_latest_revision_plural: Revisi terakhir + label_view_revisions: Tampilkan revisi + label_view_all_revisions: Tampilkan semua revisi + label_max_size: Ukuran maksimum + label_sort_highest: Ke paling atas + label_sort_higher: Ke atas + label_sort_lower: Ke bawah + label_sort_lowest: Ke paling bawah + label_roadmap: Rencana kerja + label_roadmap_due_in: "Harus selesai dalam %{value}" + label_roadmap_overdue: "%{value} terlambat" + label_roadmap_no_issues: Tak ada masalah pada versi ini + label_search: Cari + label_result_plural: Hasil + label_all_words: Semua kata + label_wiki: Wiki + label_wiki_edit: Sunting wiki + label_wiki_edit_plural: Sunting wiki + label_wiki_page: Halaman wiki + label_wiki_page_plural: Halaman wiki + label_index_by_title: Indeks menurut judul + label_index_by_date: Indeks menurut tanggal + label_current_version: Versi sekarang + label_preview: Tinjauan + label_feed_plural: Feeds + label_changes_details: Rincian semua perubahan + label_issue_tracking: Pelacak masalah + label_spent_time: Waktu terpakai + label_f_hour: "%{value} jam" + label_f_hour_plural: "%{value} jam" + label_time_tracking: Pelacak waktu + label_change_plural: Perubahan + label_statistics: Statistik + label_commits_per_month: Komit per bulan + label_commits_per_author: Komit per pengarang + label_view_diff: Tampilkan perbedaan + label_diff_inline: inline + label_diff_side_by_side: berdampingan + label_options: Pilihan + label_copy_workflow_from: Salin alur kerja dari + label_permissions_report: Laporan perijinan + label_watched_issues: Masalah terpantau + label_related_issues: Masalah terkait + label_applied_status: Status teraplikasi + label_loading: Memuat... + label_relation_new: Kaitan baru + label_relation_delete: Hapus kaitan + label_relates_to: terkait pada + label_duplicates: salinan + label_duplicated_by: disalin oleh + label_blocks: blok + label_blocked_by: diblok oleh + label_precedes: mendahului + label_follows: mengikuti + label_end_to_start: akhir ke awal + label_end_to_end: akhir ke akhir + label_start_to_start: awal ke awal + label_start_to_end: awal ke akhir + label_stay_logged_in: Tetap login + label_disabled: tidak diaktifkan + label_show_completed_versions: Tampilkan versi lengkap + label_me: saya + label_board: Forum + label_board_new: Forum baru + label_board_plural: Forum + label_topic_plural: Topik + label_message_plural: Pesan + label_message_last: Pesan terakhir + label_message_new: Pesan baru + label_message_posted: Pesan ditambahkan + label_reply_plural: Balasan + label_send_information: Kirim informasi akun ke pengguna + label_year: Tahun + label_month: Bulan + label_week: Minggu + label_date_from: Dari + label_date_to: Sampai + label_language_based: Berdasarkan bahasa pengguna + label_sort_by: "Urut berdasarkan %{value}" + label_send_test_email: Kirim email percobaan + label_feeds_access_key_created_on: "Atom access key dibuat %{value} yang lalu" + label_module_plural: Modul + label_added_time_by: "Ditambahkan oleh %{author} %{age} yang lalu" + label_updated_time_by: "Diperbarui oleh %{author} %{age} yang lalu" + label_updated_time: "Diperbarui oleh %{value} yang lalu" + label_jump_to_a_project: Pilih proyek... + label_file_plural: Berkas + label_changeset_plural: Set perubahan + label_default_columns: Kolom default + label_no_change_option: (Tak ada perubahan) + label_bulk_edit_selected_issues: Ubah masalah terpilih secara masal + label_theme: Tema + label_default: Default + label_search_titles_only: Cari judul saja + label_user_mail_option_all: "Untuk semua kejadian pada semua proyek saya" + label_user_mail_option_selected: "Hanya untuk semua kejadian pada proyek yang saya pilih ..." + label_user_mail_no_self_notified: "Saya tak ingin diberitahu untuk perubahan yang saya buat sendiri" + label_user_mail_assigned_only_mail_notification: "Kirim email hanya bila saya ditugaskan untuk masalah terkait" + label_user_mail_block_mail_notification: "Saya tidak ingin menerima email. Terima kasih." + label_registration_activation_by_email: aktivasi akun melalui email + label_registration_manual_activation: aktivasi akun secara manual + label_registration_automatic_activation: aktivasi akun secara otomatis + label_display_per_page: "Per halaman: %{value}" + label_age: Umur + label_change_properties: Rincian perubahan + label_general: Umum + label_more: Lanjut + label_scm: SCM + label_plugins: Plugin + label_ldap_authentication: Otentikasi LDAP + label_downloads_abbr: Unduh + label_optional_description: Deskripsi optional + label_add_another_file: Tambahkan berkas lain + label_preferences: Preferensi + label_chronological_order: Urut sesuai kronologis + label_reverse_chronological_order: Urut dari yang terbaru + label_planning: Perencanaan + label_incoming_emails: Email masuk + label_generate_key: Buat kunci + label_issue_watchers: Pemantau + label_example: Contoh + label_display: Tampilan + label_sort: Urut + label_ascending: Menaik + label_descending: Menurun + label_date_from_to: Dari %{start} sampai %{end} + label_wiki_content_added: Halaman wiki ditambahkan + label_wiki_content_updated: Halaman wiki diperbarui + label_group: Kelompok + label_group_plural: Kelompok + label_group_new: Kelompok baru + label_time_entry_plural: Waktu terpakai + label_version_sharing_none: Tidak dibagi + label_version_sharing_descendants: Dengan subproyek + label_version_sharing_hierarchy: Dengan hirarki proyek + label_version_sharing_tree: Dengan pohon proyek + label_version_sharing_system: Dengan semua proyek + + + button_login: Login + button_submit: Kirim + button_save: Simpan + button_check_all: Contreng semua + button_uncheck_all: Hilangkan semua contreng + button_delete: Hapus + button_create: Buat + button_create_and_continue: Buat dan lanjutkan + button_test: Test + button_edit: Sunting + button_add: Tambahkan + button_change: Ubah + button_apply: Terapkan + button_clear: Bersihkan + button_lock: Kunci + button_unlock: Buka kunci + button_download: Unduh + button_list: Daftar + button_view: Tampilkan + button_move: Pindah + button_move_and_follow: Pindah dan ikuti + button_back: Kembali + button_cancel: Batal + button_activate: Aktifkan + button_sort: Urut + button_log_time: Rekam waktu + button_rollback: Kembali ke versi ini + button_watch: Pantau + button_unwatch: Tidak Memantau + button_reply: Balas + button_archive: Arsip + button_unarchive: Batalkan arsip + button_reset: Reset + button_rename: Ganti nama + button_change_password: Ubah kata sandi + button_copy: Salin + button_copy_and_follow: Salin dan ikuti + button_annotate: Anotasi + button_update: Perbarui + button_configure: Konfigur + button_quote: Kutip + button_duplicate: Duplikat + + status_active: aktif + status_registered: terdaftar + status_locked: terkunci + + version_status_open: terbuka + version_status_locked: terkunci + version_status_closed: tertutup + + field_active: Aktif + + text_select_mail_notifications: Pilih aksi dimana email notifikasi akan dikirimkan. + text_regexp_info: mis. ^[A-Z0-9]+$ + text_min_max_length_info: 0 berarti tidak ada pembatasan + text_project_destroy_confirmation: Apakah anda benar-benar akan menghapus proyek ini beserta data terkait ? + text_subprojects_destroy_warning: "Subproyek: %{value} juga akan dihapus." + text_workflow_edit: Pilih peran dan pelacak untuk menyunting alur kerja + text_are_you_sure: Anda yakin ? + text_journal_changed: "%{label} berubah dari %{old} menjadi %{new}" + text_journal_set_to: "%{label} di set ke %{value}" + text_journal_deleted: "%{label} dihapus (%{old})" + text_journal_added: "%{label} %{value} ditambahkan" + text_tip_issue_begin_day: tugas dimulai hari itu + text_tip_issue_end_day: tugas berakhir hari itu + text_tip_issue_begin_end_day: tugas dimulai dan berakhir hari itu + text_caracters_maximum: "maximum %{count} karakter." + text_caracters_minimum: "Setidaknya harus sepanjang %{count} karakter." + text_length_between: "Panjang diantara %{min} dan %{max} karakter." + text_tracker_no_workflow: Tidak ada alur kerja untuk pelacak ini + text_unallowed_characters: Karakter tidak diperbolehkan + text_comma_separated: Beberapa nilai diperbolehkan (dipisahkan koma). + text_issues_ref_in_commit_messages: Mereferensikan dan membetulkan masalah pada pesan komit + text_issue_added: "Masalah %{id} sudah dilaporkan oleh %{author}." + text_issue_updated: "Masalah %{id} sudah diperbarui oleh %{author}." + text_wiki_destroy_confirmation: Apakah anda benar-benar akan menghapus wiki ini beserta semua isinya ? + text_issue_category_destroy_question: "Beberapa masalah (%{count}) ditugaskan pada kategori ini. Apa yang anda lakukan ?" + text_issue_category_destroy_assignments: Hapus kategori penugasan + text_issue_category_reassign_to: Tugaskan kembali masalah untuk kategori ini + text_user_mail_option: "Untuk proyek yang tidak dipilih, anda hanya akan menerima notifikasi hal-hal yang anda pantau atau anda terlibat di dalamnya (misalnya masalah yang anda tulis atau ditugaskan pada anda)." + text_no_configuration_data: "Peran, pelacak, status masalah dan alur kerja belum dikonfigur.\nSangat disarankan untuk memuat konfigurasi default. Anda akan bisa mengubahnya setelah konfigurasi dimuat." + text_load_default_configuration: Muat konfigurasi default + text_status_changed_by_changeset: "Diterapkan di set perubahan %{value}." + text_issues_destroy_confirmation: 'Apakah anda yakin untuk menghapus masalah terpilih ?' + text_select_project_modules: 'Pilih modul untuk diaktifkan pada proyek ini:' + text_default_administrator_account_changed: Akun administrator default sudah berubah + text_file_repository_writable: Direktori yang bisa ditulisi untuk lampiran + text_plugin_assets_writable: Direktori yang bisa ditulisi untuk plugin asset + text_rmagick_available: RMagick tersedia (optional) + text_destroy_time_entries_question: "%{hours} jam sudah dilaporkan pada masalah yang akan anda hapus. Apa yang akan anda lakukan ?" + text_destroy_time_entries: Hapus jam yang terlapor + text_assign_time_entries_to_project: Tugaskan jam terlapor pada proyek + text_reassign_time_entries: 'Tugaskan kembali jam terlapor pada masalah ini:' + text_user_wrote: "%{value} menulis:" + text_enumeration_destroy_question: "%{count} obyek ditugaskan untuk nilai ini." + text_enumeration_category_reassign_to: 'Tugaskan kembali untuk nilai ini:' + text_email_delivery_not_configured: "Pengiriman email belum dikonfigurasi, notifikasi tidak diaktifkan.\nAnda harus mengkonfigur SMTP server anda pada config/configuration.yml dan restart kembali aplikasi untuk mengaktifkan." + text_repository_usernames_mapping: "Pilih atau perbarui pengguna Redmine yang terpetakan ke setiap nama pengguna yang ditemukan di log repositori.\nPengguna dengan nama pengguna dan repositori atau email yang sama secara otomasit akan dipetakan." + text_diff_truncated: '... Perbedaan terpotong karena melebihi batas maksimum yang bisa ditampilkan.' + text_custom_field_possible_values_info: 'Satu baris untuk setiap nilai' + text_wiki_page_destroy_question: "Halaman ini mempunyai %{descendants} halaman anak dan turunannya. Apa yang akan anda lakukan ?" + text_wiki_page_nullify_children: "Biarkan halaman anak sebagai halaman teratas (root)" + text_wiki_page_destroy_children: "Hapus halaman anak dan semua turunannya" + text_wiki_page_reassign_children: "Tujukan halaman anak ke halaman induk yang ini" + + default_role_manager: Manager + default_role_developer: Pengembang + default_role_reporter: Pelapor + default_tracker_bug: Bug + default_tracker_feature: Fitur + default_tracker_support: Dukungan + default_issue_status_new: Baru + default_issue_status_in_progress: Dalam proses + default_issue_status_resolved: Resolved + default_issue_status_feedback: Umpan balik + default_issue_status_closed: Ditutup + default_issue_status_rejected: Ditolak + default_doc_category_user: Dokumentasi pengguna + default_doc_category_tech: Dokumentasi teknis + default_priority_low: Rendah + default_priority_normal: Normal + default_priority_high: Tinggi + default_priority_urgent: Penting + default_priority_immediate: Segera + default_activity_design: Rancangan + default_activity_development: Pengembangan + + enumeration_issue_priorities: Prioritas masalah + enumeration_doc_categories: Kategori dokumen + enumeration_activities: Kegiatan + enumeration_system_activity: Kegiatan Sistem + label_copy_source: Source + label_update_issue_done_ratios: Update issue done ratios + setting_issue_done_ratio: Calculate the issue done ratio with + label_api_access_key: API access key + text_line_separated: Multiple values allowed (one line for each value). + label_revision_id: Revision %{value} + permission_view_issues: View Issues + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + label_display_used_statuses_only: Only display statuses that are used by this tracker + error_workflow_copy_target: Please select target tracker(s) and role(s) + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: Atom access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_copy_same_as_target: Same as target + button_show: Show + setting_issue_done_ratio_issue_field: Use the issue field + label_missing_api_access_key: Missing an API access key + label_copy_target: Target + label_missing_feeds_access_key: Missing a Atom access key + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + setting_start_of_week: Start calendars on + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: Commit messages 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 masalah + one: 1 masalah + other: "%{count} masalah" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: semua + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Dengan subproyek + label_cross_project_tree: Dengan pohon proyek + label_cross_project_hierarchy: Dengan hirarki proyek + label_cross_project_system: Dengan semua proyek + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Total + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/24/241cbe0d1433ee9e75de970381ea7ac06108d9a9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/24/241cbe0d1433ee9e75de970381ea7ac06108d9a9.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,68 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 WikiContentVersionTest < ActiveSupport::TestCase + fixtures :projects, :users, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions + + def setup + end + + def test_destroy + v = WikiContent::Version.find(2) + + assert_difference 'WikiContent::Version.count', -1 do + v.destroy + end + end + + def test_destroy_last_version_should_revert_content + v = WikiContent::Version.find(3) + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count', -1 do + assert v.destroy + end + end + end + c = WikiContent.find(1) + v = c.versions.last + assert_equal 2, c.version + assert_equal v.version, c.version + assert_equal v.comments, c.comments + assert_equal v.text, c.text + assert_equal v.author, c.author + assert_equal v.updated_on, c.updated_on + end + + def test_destroy_all_versions_should_delete_page + WikiContent::Version.find(1).destroy + WikiContent::Version.find(2).destroy + v = WikiContent::Version.find(3) + + assert_difference 'WikiPage.count', -1 do + assert_difference 'WikiContent.count', -1 do + assert_difference 'WikiContent::Version.count', -1 do + assert v.destroy + end + end + end + assert_nil WikiPage.find_by_id(1) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/24/246ffef1af2cdf25b19b8c7e630a8133661d91bf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/24/246ffef1af2cdf25b19b8c7e630a8133661d91bf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,147 @@ +--- +custom_fields_001: + name: Database + regexp: "" + is_for_all: true + is_filter: true + type: IssueCustomField + possible_values: + - MySQL + - PostgreSQL + - Oracle + id: 1 + is_required: false + field_format: list + default_value: "" + editable: true + position: 2 +custom_fields_002: + name: Searchable field + min_length: 1 + regexp: "" + is_for_all: true + is_filter: true + type: IssueCustomField + max_length: 100 + possible_values: "" + id: 2 + is_required: false + field_format: string + searchable: true + default_value: "Default string" + editable: true + position: 1 +custom_fields_003: + name: Development status + regexp: "" + is_for_all: false + is_filter: true + type: ProjectCustomField + possible_values: + - Stable + - Beta + - Alpha + - Planning + id: 3 + is_required: false + field_format: list + default_value: "" + editable: true + position: 1 +custom_fields_004: + name: Phone number + regexp: "" + is_for_all: false + type: UserCustomField + possible_values: "" + id: 4 + is_required: false + field_format: string + default_value: "" + editable: true + position: 1 +custom_fields_005: + name: Money + regexp: "" + is_for_all: false + type: UserCustomField + possible_values: "" + id: 5 + is_required: false + field_format: float + default_value: "" + editable: true + position: 2 +custom_fields_006: + name: Float field + regexp: "" + is_for_all: true + type: IssueCustomField + possible_values: "" + id: 6 + is_required: false + field_format: float + default_value: "" + editable: true + position: 3 +custom_fields_007: + name: Billable + regexp: "" + is_for_all: false + is_filter: true + type: TimeEntryActivityCustomField + possible_values: "" + id: 7 + is_required: false + field_format: bool + default_value: "" + editable: true + position: 1 +custom_fields_008: + name: Custom date + regexp: "" + is_for_all: true + is_filter: false + type: IssueCustomField + possible_values: "" + id: 8 + is_required: false + field_format: date + default_value: "" + editable: true + position: 4 +custom_fields_009: + name: Project 1 cf + regexp: "" + is_for_all: false + is_filter: true + type: IssueCustomField + possible_values: "" + id: 9 + is_required: false + field_format: date + default_value: "" + editable: true + position: 5 +custom_fields_010: + name: Overtime + regexp: "" + is_for_all: false + is_filter: false + type: TimeEntryCustomField + possible_values: "" + id: 10 + is_required: false + field_format: bool + default_value: 0 + editable: true + position: 1 +custom_fields_011: + id: 11 + name: Binary + type: CustomField + possible_values: + - !binary | + SGXDqWzDp2prc2Tigqw2NTTDuQ== + - Other value + field_format: list diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/24/24ef36c7147955d3fac8e868fb99fab632f24089.svn-base --- a/.svn/pristine/24/24ef36c7147955d3fac8e868fb99fab632f24089.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 LayoutTest < ActionController::IntegrationTest - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows - - test "browsing to a missing page should render the base layout" do - get "/users/100000000" - - assert_response :not_found - - # UsersController uses the admin layout by default - assert_select "#admin-menu", :count => 0 - end - - test "browsing to an unauthorized page should render the base layout" do - change_user_password('miscuser9', 'test1234') - - log_user('miscuser9','test1234') - - get "/admin" - assert_response :forbidden - assert_select "#admin-menu", :count => 0 - end - - def test_top_menu_and_search_not_visible_when_login_required - with_settings :login_required => '1' do - get '/' - assert_select "#top-menu > ul", 0 - assert_select "#quick-search", 0 - end - end - - def test_top_menu_and_search_visible_when_login_not_required - with_settings :login_required => '0' do - get '/' - assert_select "#top-menu > ul" - assert_select "#quick-search" - end - end - - def test_wiki_formatter_header_tags - Role.anonymous.add_permission! :add_issues - - get '/projects/ecookbook/issues/new' - assert_tag :script, - :attributes => {:src => %r{^/javascripts/jstoolbar/jstoolbar-textile.min.js}}, - :parent => {:tag => 'head'} - end - - def test_calendar_header_tags - with_settings :default_language => 'fr' do - get '/issues' - assert_include "/javascripts/i18n/jquery.ui.datepicker-fr.js", response.body - end - - with_settings :default_language => 'en-GB' do - get '/issues' - assert_include "/javascripts/i18n/jquery.ui.datepicker-en-GB.js", response.body - end - - with_settings :default_language => 'en' do - get '/issues' - assert_not_include "/javascripts/i18n/jquery.ui.datepicker", response.body - end - end - - def test_search_field_outside_project_should_link_to_global_search - get '/' - assert_select 'div#quick-search form[action=/search]' - end - - def test_search_field_inside_project_should_link_to_project_search - get '/projects/ecookbook' - assert_select 'div#quick-search form[action=/projects/ecookbook/search]' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/252eb4f2f106201746352d88a816534e53e8ac4b.svn-base --- a/.svn/pristine/25/252eb4f2f106201746352d88a816534e53e8ac4b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 #:nodoc: - module CoreExtensions #:nodoc: - module String #:nodoc: - # Custom string conversions - module Conversions - # Parses hours format and returns a float - def to_hours - s = self.dup - s.strip! - if s =~ %r{^(\d+([.,]\d+)?)h?$} - s = $1 - else - # 2:30 => 2.5 - s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 } - # 2h30, 2h, 30m => 2.5, 2, 0.5 - s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}i) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] } - end - # 2,5 => 2.5 - s.gsub!(',', '.') - begin; Kernel.Float(s); rescue; nil; end - end - - # Object#to_a removed in ruby1.9 - if RUBY_VERSION > '1.9' - def to_a - [self.dup] - end - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25444d6bb6def1b64d15ccc601db528a2e5aca60.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25444d6bb6def1b64d15ccc601db528a2e5aca60.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1406 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 QueryTest < ActiveSupport::TestCase + include Redmine::I18n + + fixtures :projects, :enabled_modules, :users, :members, + :member_roles, :roles, :trackers, :issue_statuses, + :issue_categories, :enumerations, :issues, + :watchers, :custom_fields, :custom_values, :versions, + :queries, + :projects_trackers, + :custom_fields_trackers + + def test_query_with_roles_visibility_should_validate_roles + set_language_if_valid 'en' + query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES) + assert !query.save + assert_include "Roles can't be blank", query.errors.full_messages + query.role_ids = [1, 2] + assert query.save + end + + def test_changing_roles_visibility_should_clear_roles + query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2]) + assert_equal 2, query.roles.count + + query.visibility = IssueQuery::VISIBILITY_PUBLIC + query.save! + assert_equal 0, query.roles.count + end + + def test_available_filters_should_be_ordered + set_language_if_valid 'en' + query = IssueQuery.new + assert_equal 0, query.available_filters.keys.index('status_id') + expected_order = [ + "Status", + "Project", + "Tracker", + "Priority" + ] + assert_equal expected_order, + (query.available_filters.values.map{|v| v[:name]} & expected_order) + end + + def test_available_filters_with_custom_fields_should_be_ordered + set_language_if_valid 'en' + UserCustomField.create!( + :name => 'order test', :field_format => 'string', + :is_for_all => true, :is_filter => true + ) + query = IssueQuery.new + expected_order = [ + "Searchable field", + "Database", + "Project's Development status", + "Author's order test", + "Assignee's order test" + ] + assert_equal expected_order, + (query.available_filters.values.map{|v| v[:name]} & expected_order) + end + + def test_custom_fields_for_all_projects_should_be_available_in_global_queries + query = IssueQuery.new(:project => nil, :name => '_') + assert query.available_filters.has_key?('cf_1') + assert !query.available_filters.has_key?('cf_3') + end + + def test_system_shared_versions_should_be_available_in_global_queries + Version.find(2).update_attribute :sharing, 'system' + query = IssueQuery.new(:project => nil, :name => '_') + assert query.available_filters.has_key?('fixed_version_id') + assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'} + end + + def test_project_filter_in_global_queries + query = IssueQuery.new(:project => nil, :name => '_') + project_filter = query.available_filters["project_id"] + assert_not_nil project_filter + project_ids = project_filter[:values].map{|p| p[1]} + assert project_ids.include?("1") #public project + assert !project_ids.include?("2") #private project user cannot see + end + + def find_issues_with_query(query) + Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( + query.statement + ).all + end + + def assert_find_issues_with_query_is_successful(query) + assert_nothing_raised do + find_issues_with_query(query) + end + end + + def assert_query_statement_includes(query, condition) + assert_include condition, query.statement + end + + def assert_query_result(expected, query) + assert_nothing_raised do + assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort + assert_equal expected.size, query.issue_count + end + end + + def test_query_should_allow_shared_versions_for_a_project_query + subproject_version = Version.find(4) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s]) + + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')") + end + + def test_query_with_multiple_custom_fields + query = IssueQuery.find(1) + assert query.valid? + assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')") + issues = find_issues_with_query(query) + assert_equal 1, issues.length + assert_equal Issue.find(3), issues.first + end + + def test_operator_none + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '!*', ['']) + query.add_filter('cf_1', '!*', ['']) + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL") + assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''") + find_issues_with_query(query) + end + + def test_operator_none_for_integer + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('estimated_hours', '!*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| !i.estimated_hours} + end + + def test_operator_none_for_date + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('start_date', '!*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.start_date.nil?} + end + + def test_operator_none_for_string_custom_field + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('cf_2', '!*', ['']) + assert query.has_filter?('cf_2') + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.custom_field_value(2).blank?} + end + + def test_operator_all + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '*', ['']) + query.add_filter('cf_1', '*', ['']) + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL") + assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''") + find_issues_with_query(query) + end + + def test_operator_all_for_date + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('start_date', '*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.start_date.present?} + end + + def test_operator_all_for_string_custom_field + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('cf_2', '*', ['']) + assert query.has_filter?('cf_2') + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.custom_field_value(2).present?} + end + + def test_numeric_filter_should_not_accept_non_numeric_values + query = IssueQuery.new(:name => '_') + query.add_filter('estimated_hours', '=', ['a']) + + assert query.has_filter?('estimated_hours') + assert !query.valid? + end + + def test_operator_is_on_float + Issue.update_all("estimated_hours = 171.2", "id=2") + + query = IssueQuery.new(:name => '_') + query.add_filter('estimated_hours', '=', ['171.20']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_integer_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['12']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_integer_custom_field_should_accept_negative_value + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['-12']) + assert query.valid? + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_float_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['12.7']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_float_custom_field_should_accept_negative_value + f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['-12.7']) + assert query.valid? + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_multi_list_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true, + :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1') + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1') + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['value1']) + issues = find_issues_with_query(query) + assert_equal [1, 3], issues.map(&:id).sort + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['value2']) + issues = find_issues_with_query(query) + assert_equal [1], issues.map(&:id).sort + end + + def test_operator_is_not_on_multi_list_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true, + :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1') + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1') + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '!', ['value1']) + issues = find_issues_with_query(query) + assert !issues.map(&:id).include?(1) + assert !issues.map(&:id).include?(3) + + query = IssueQuery.new(:name => '_') + query.add_filter("cf_#{f.id}", '!', ['value2']) + issues = find_issues_with_query(query) + assert !issues.map(&:id).include?(1) + assert issues.map(&:id).include?(3) + end + + def test_operator_is_on_is_private_field + # is_private filter only available for those who can set issues private + User.current = User.find(2) + + query = IssueQuery.new(:name => '_') + assert query.available_filters.key?('is_private') + + query.add_filter("is_private", '=', ['1']) + issues = find_issues_with_query(query) + assert issues.any? + assert_nil issues.detect {|issue| !issue.is_private?} + ensure + User.current = nil + end + + def test_operator_is_not_on_is_private_field + # is_private filter only available for those who can set issues private + User.current = User.find(2) + + query = IssueQuery.new(:name => '_') + assert query.available_filters.key?('is_private') + + query.add_filter("is_private", '!', ['1']) + issues = find_issues_with_query(query) + assert issues.any? + assert_nil issues.detect {|issue| issue.is_private?} + ensure + User.current = nil + end + + def test_operator_greater_than + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '>=', ['40']) + assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0") + find_issues_with_query(query) + end + + def test_operator_greater_than_a_float + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('estimated_hours', '>=', ['40.5']) + assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5") + find_issues_with_query(query) + end + + def test_operator_greater_than_on_int_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '>=', ['8']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_lesser_than + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '<=', ['30']) + assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0") + find_issues_with_query(query) + end + + def test_operator_lesser_than_on_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '<=', ['30']) + assert_match /CAST.+ <= 30\.0/, query.statement + find_issues_with_query(query) + end + + def test_operator_lesser_than_on_date_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '<=', ['2013-05-01']) + issue_ids = find_issues_with_query(query).map(&:id) + assert_include 1, issue_ids + assert_not_include 2, issue_ids + assert_not_include 3, issue_ids + end + + def test_operator_between + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '><', ['30', '40']) + assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement + find_issues_with_query(query) + end + + def test_operator_between_on_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '><', ['30', '40']) + assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement + find_issues_with_query(query) + end + + def test_date_filter_should_not_accept_non_date_values + query = IssueQuery.new(:name => '_') + query.add_filter('created_on', '=', ['a']) + + assert query.has_filter?('created_on') + assert !query.valid? + end + + def test_date_filter_should_not_accept_invalid_date_values + query = IssueQuery.new(:name => '_') + query.add_filter('created_on', '=', ['2011-01-34']) + + assert query.has_filter?('created_on') + assert !query.valid? + end + + def test_relative_date_filter_should_not_accept_non_integer_values + query = IssueQuery.new(:name => '_') + query.add_filter('created_on', '>t-', ['a']) + + assert query.has_filter?('created_on') + assert !query.valid? + end + + def test_operator_date_equals + query = IssueQuery.new(:name => '_') + query.add_filter('due_date', '=', ['2011-07-10']) + assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement + find_issues_with_query(query) + end + + def test_operator_date_lesser_than + query = IssueQuery.new(:name => '_') + query.add_filter('due_date', '<=', ['2011-07-10']) + assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement + find_issues_with_query(query) + end + + def test_operator_date_greater_than + query = IssueQuery.new(:name => '_') + query.add_filter('due_date', '>=', ['2011-07-10']) + assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement + find_issues_with_query(query) + end + + def test_operator_date_between + query = IssueQuery.new(:name => '_') + query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10']) + assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?'/, query.statement + find_issues_with_query(query) + end + + def test_operator_in_more_than + Issue.find(7).update_attribute(:due_date, (Date.today + 15)) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>t+', ['15']) + issues = find_issues_with_query(query) + assert !issues.empty? + issues.each {|issue| assert(issue.due_date >= (Date.today + 15))} + end + + def test_operator_in_less_than + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', ' Project.find(1), :name => '_') + query.add_filter('due_date', '>= Date.today && issue.due_date <= (Date.today + 15))} + end + + def test_operator_less_than_ago + Issue.find(7).update_attribute(:due_date, (Date.today - 3)) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>t-', ['3']) + issues = find_issues_with_query(query) + assert !issues.empty? + issues.each {|issue| assert(issue.due_date >= (Date.today - 3))} + end + + def test_operator_in_the_past_days + Issue.find(7).update_attribute(:due_date, (Date.today - 3)) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>= (Date.today - 3) && issue.due_date <= Date.today)} + end + + def test_operator_more_than_ago + Issue.find(7).update_attribute(:due_date, (Date.today - 10)) + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', ' Project.find(1), :name => '_') + query.add_filter('due_date', 'w', ['']) + assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}" + I18n.locale = :en + end + + def test_range_for_this_week_with_week_starting_on_sunday + I18n.locale = :en + assert_equal '7', I18n.t(:general_first_day_of_week) + + Date.stubs(:today).returns(Date.parse('2011-04-29')) + + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', 'w', ['']) + assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}" + end + + def test_operator_does_not_contains + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter('subject', '!~', ['uNable']) + assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'") + find_issues_with_query(query) + end + + def test_filter_assigned_to_me + user = User.find(2) + group = Group.find(10) + User.current = user + i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user) + i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group) + i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11)) + group.users << user + + query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}}) + result = query.issues + assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id) + + assert result.include?(i1) + assert result.include?(i2) + assert !result.include?(i3) + end + + def test_user_custom_field_filtered_on_me + User.current = User.find(2) + cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1]) + issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1) + issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'}) + + query = IssueQuery.new(:name => '_', :project => Project.find(1)) + filter = query.available_filters["cf_#{cf.id}"] + assert_not_nil filter + assert_include 'me', filter[:values].map{|v| v[1]} + + query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}} + result = query.issues + assert_equal 1, result.size + assert_equal issue1, result.first + end + + def test_filter_my_projects + User.current = User.find(2) + query = IssueQuery.new(:name => '_') + filter = query.available_filters['project_id'] + assert_not_nil filter + assert_include 'mine', filter[:values].map{|v| v[1]} + + query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}} + result = query.issues + assert_nil result.detect {|issue| !User.current.member_of?(issue.project)} + end + + def test_filter_watched_issues + User.current = User.find(1) + query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}}) + result = find_issues_with_query(query) + assert_not_nil result + assert !result.empty? + assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id) + User.current = nil + end + + def test_filter_unwatched_issues + User.current = User.find(1) + query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}}) + result = find_issues_with_query(query) + assert_not_nil result + assert !result.empty? + assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size) + User.current = nil + end + + def test_filter_on_custom_field_should_ignore_projects_with_field_disabled + field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_filter => true) + Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'}) + Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'}) + + query = IssueQuery.new(:name => '_', :project => Project.find(1)) + query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}} + assert_equal 2, find_issues_with_query(query).size + + field.project_ids = [1, 3] # Disable the field for project 4 + field.save! + assert_equal 1, find_issues_with_query(query).size + end + + def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled + field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true) + Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'}) + Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'}) + + query = IssueQuery.new(:name => '_', :project => Project.find(1)) + query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}} + assert_equal 2, find_issues_with_query(query).size + + field.tracker_ids = [1] # Disable the field for tracker 2 + field.save! + assert_equal 1, find_issues_with_query(query).size + end + + def test_filter_on_project_custom_field + field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo') + CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo') + + query = IssueQuery.new(:name => '_') + filter_name = "project.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort + end + + def test_filter_on_author_custom_field + field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo') + + query = IssueQuery.new(:name => '_') + filter_name = "author.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort + end + + def test_filter_on_assigned_to_custom_field + field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo') + + query = IssueQuery.new(:name => '_') + filter_name = "assigned_to.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort + end + + def test_filter_on_fixed_version_custom_field + field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo') + + query = IssueQuery.new(:name => '_') + filter_name = "fixed_version.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort + end + + def test_filter_on_relations_with_a_specific_issue + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '=', :values => ['1']}} + assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '=', :values => ['2']}} + assert_equal [1], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_with_any_issues_in_a_project + IssueRelation.delete_all + with_settings :cross_project_issue_relations => '1' do + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first) + end + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '=p', :values => ['2']}} + assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '=p', :values => ['3']}} + assert_equal [1], find_issues_with_query(query).map(&:id).sort + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '=p', :values => ['4']}} + assert_equal [], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_with_any_issues_not_in_a_project + IssueRelation.delete_all + with_settings :cross_project_issue_relations => '1' do + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first) + #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first) + end + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '=!p', :values => ['1']}} + assert_equal [1], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_with_no_issues_in_a_project + IssueRelation.delete_all + with_settings :cross_project_issue_relations => '1' do + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3)) + end + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '!p', :values => ['2']}} + ids = find_issues_with_query(query).map(&:id).sort + assert_include 2, ids + assert_not_include 1, ids + assert_not_include 3, ids + end + + def test_filter_on_relations_with_no_issues + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '!*', :values => ['']}} + ids = find_issues_with_query(query).map(&:id) + assert_equal [], ids & [1, 2, 3] + assert_include 4, ids + end + + def test_filter_on_relations_with_any_issues + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) + + query = IssueQuery.new(:name => '_') + query.filters = {"relates" => {:operator => '*', :values => ['']}} + assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_should_not_ignore_other_filter + issue = Issue.generate! + issue1 = Issue.generate!(:status_id => 1) + issue2 = Issue.generate!(:status_id => 2) + IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1) + IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2) + + query = IssueQuery.new(:name => '_') + query.filters = { + "status_id" => {:operator => '=', :values => ['1']}, + "relates" => {:operator => '=', :values => [issue.id.to_s]} + } + assert_equal [issue1], find_issues_with_query(query) + end + + def test_statement_should_be_nil_with_no_filters + q = IssueQuery.new(:name => '_') + q.filters = {} + + assert q.valid? + assert_nil q.statement + end + + def test_default_columns + q = IssueQuery.new + assert q.columns.any? + assert q.inline_columns.any? + assert q.block_columns.empty? + end + + def test_set_column_names + q = IssueQuery.new + q.column_names = ['tracker', :subject, '', 'unknonw_column'] + assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name} + end + + def test_has_column_should_accept_a_column_name + q = IssueQuery.new + q.column_names = ['tracker', :subject] + assert q.has_column?(:tracker) + assert !q.has_column?(:category) + end + + def test_has_column_should_accept_a_column + q = IssueQuery.new + q.column_names = ['tracker', :subject] + + tracker_column = q.available_columns.detect {|c| c.name==:tracker} + assert_kind_of QueryColumn, tracker_column + category_column = q.available_columns.detect {|c| c.name==:category} + assert_kind_of QueryColumn, category_column + + assert q.has_column?(tracker_column) + assert !q.has_column?(category_column) + end + + def test_inline_and_block_columns + q = IssueQuery.new + q.column_names = ['subject', 'description', 'tracker'] + + assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name) + assert_equal [:description], q.block_columns.map(&:name) + end + + def test_custom_field_columns_should_be_inline + q = IssueQuery.new + columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn} + assert columns.any? + assert_nil columns.detect {|column| !column.inline?} + end + + def test_query_should_preload_spent_hours + q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours]) + assert q.has_column?(:spent_hours) + issues = q.issues + assert_not_nil issues.first.instance_variable_get("@spent_hours") + end + + def test_groupable_columns_should_include_custom_fields + q = IssueQuery.new + column = q.groupable_columns.detect {|c| c.name == :cf_1} + assert_not_nil column + assert_kind_of QueryCustomFieldColumn, column + end + + def test_groupable_columns_should_not_include_multi_custom_fields + field = CustomField.find(1) + field.update_attribute :multiple, true + + q = IssueQuery.new + column = q.groupable_columns.detect {|c| c.name == :cf_1} + assert_nil column + end + + def test_groupable_columns_should_include_user_custom_fields + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user') + + q = IssueQuery.new + assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym} + end + + def test_groupable_columns_should_include_version_custom_fields + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version') + + q = IssueQuery.new + assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym} + end + + def test_grouped_with_valid_column + q = IssueQuery.new(:group_by => 'status') + assert q.grouped? + assert_not_nil q.group_by_column + assert_equal :status, q.group_by_column.name + assert_not_nil q.group_by_statement + assert_equal 'status', q.group_by_statement + end + + def test_grouped_with_invalid_column + q = IssueQuery.new(:group_by => 'foo') + assert !q.grouped? + assert_nil q.group_by_column + assert_nil q.group_by_statement + end + + def test_sortable_columns_should_sort_assignees_according_to_user_format_setting + with_settings :user_format => 'lastname_coma_firstname' do + q = IssueQuery.new + assert q.sortable_columns.has_key?('assigned_to') + assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to'] + end + end + + def test_sortable_columns_should_sort_authors_according_to_user_format_setting + with_settings :user_format => 'lastname_coma_firstname' do + q = IssueQuery.new + assert q.sortable_columns.has_key?('author') + assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author'] + end + end + + def test_sortable_columns_should_include_custom_field + q = IssueQuery.new + assert q.sortable_columns['cf_1'] + end + + def test_sortable_columns_should_not_include_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + + q = IssueQuery.new + assert !q.sortable_columns['cf_1'] + end + + def test_default_sort + q = IssueQuery.new + assert_equal [], q.sort_criteria + end + + def test_set_sort_criteria_with_hash + q = IssueQuery.new + q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']} + assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria + end + + def test_set_sort_criteria_with_array + q = IssueQuery.new + q.sort_criteria = [['priority', 'desc'], 'tracker'] + assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria + end + + def test_create_query_with_sort + q = IssueQuery.new(:name => 'Sorted') + q.sort_criteria = [['priority', 'desc'], 'tracker'] + assert q.save + q.reload + assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria + end + + def test_sort_by_string_custom_field_asc + q = IssueQuery.new + c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' } + assert c + assert c.sortable + issues = q.issues(:order => "#{c.sortable} ASC") + values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s} + assert !values.empty? + assert_equal values.sort, values + end + + def test_sort_by_string_custom_field_desc + q = IssueQuery.new + c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' } + assert c + assert c.sortable + issues = q.issues(:order => "#{c.sortable} DESC") + values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s} + assert !values.empty? + assert_equal values.sort.reverse, values + end + + def test_sort_by_float_custom_field_asc + q = IssueQuery.new + c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' } + assert c + assert c.sortable + issues = q.issues(:order => "#{c.sortable} ASC") + values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact + assert !values.empty? + assert_equal values.sort, values + end + + def test_invalid_query_should_raise_query_statement_invalid_error + q = IssueQuery.new + assert_raise Query::StatementInvalid do + q.issues(:conditions => "foo = 1") + end + end + + def test_issue_count + q = IssueQuery.new(:name => '_') + issue_count = q.issue_count + assert_equal q.issues.size, issue_count + end + + def test_issue_count_with_archived_issues + p = Project.generate! do |project| + project.status = Project::STATUS_ARCHIVED + end + i = Issue.generate!( :project => p, :tracker => p.trackers.first ) + assert !i.visible? + + test_issue_count + end + + def test_issue_count_by_association_group + q = IssueQuery.new(:name => '_', :group_by => 'assigned_to') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort + assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq + assert count_by_group.has_key?(User.find(3)) + end + + def test_issue_count_by_list_custom_field_group + q = IssueQuery.new(:name => '_', :group_by => 'cf_1') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort + assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq + assert count_by_group.has_key?('MySQL') + end + + def test_issue_count_by_date_custom_field_group + q = IssueQuery.new(:name => '_', :group_by => 'cf_8') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort + assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq + end + + def test_issue_count_with_nil_group_only + Issue.update_all("assigned_to_id = NULL") + + q = IssueQuery.new(:name => '_', :group_by => 'assigned_to') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal 1, count_by_group.keys.size + assert_nil count_by_group.keys.first + end + + def test_issue_ids + q = IssueQuery.new(:name => '_') + order = "issues.subject, issues.id" + issues = q.issues(:order => order) + assert_equal issues.map(&:id), q.issue_ids(:order => order) + end + + def test_label_for + set_language_if_valid 'en' + q = IssueQuery.new + assert_equal 'Assignee', q.label_for('assigned_to_id') + end + + def test_label_for_fr + set_language_if_valid 'fr' + q = IssueQuery.new + s = "Assign\xc3\xa9 \xc3\xa0" + s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) + assert_equal s, q.label_for('assigned_to_id') + end + + def test_editable_by + admin = User.find(1) + manager = User.find(2) + developer = User.find(3) + + # Public query on project 1 + q = IssueQuery.find(1) + assert q.editable_by?(admin) + assert q.editable_by?(manager) + assert !q.editable_by?(developer) + + # Private query on project 1 + q = IssueQuery.find(2) + assert q.editable_by?(admin) + assert !q.editable_by?(manager) + assert q.editable_by?(developer) + + # Private query for all projects + q = IssueQuery.find(3) + assert q.editable_by?(admin) + assert !q.editable_by?(manager) + assert q.editable_by?(developer) + + # Public query for all projects + q = IssueQuery.find(4) + assert q.editable_by?(admin) + assert !q.editable_by?(manager) + assert !q.editable_by?(developer) + end + + def test_visible_scope + query_ids = IssueQuery.visible(User.anonymous).map(&:id) + + assert query_ids.include?(1), 'public query on public project was not visible' + assert query_ids.include?(4), 'public query for all projects was not visible' + assert !query_ids.include?(2), 'private query on public project was visible' + assert !query_ids.include?(3), 'private query for all projects was visible' + assert !query_ids.include?(7), 'public query on private project was visible' + end + + def test_query_with_public_visibility_should_be_visible_to_anyone + q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC) + + assert q.visible?(User.anonymous) + assert IssueQuery.visible(User.anonymous).find_by_id(q.id) + + assert q.visible?(User.find(7)) + assert IssueQuery.visible(User.find(7)).find_by_id(q.id) + + assert q.visible?(User.find(2)) + assert IssueQuery.visible(User.find(2)).find_by_id(q.id) + + assert q.visible?(User.find(1)) + assert IssueQuery.visible(User.find(1)).find_by_id(q.id) + end + + def test_query_with_roles_visibility_should_be_visible_to_user_with_role + q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2]) + + assert !q.visible?(User.anonymous) + assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id) + + assert !q.visible?(User.find(7)) + assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id) + + assert q.visible?(User.find(2)) + assert IssueQuery.visible(User.find(2)).find_by_id(q.id) + + assert q.visible?(User.find(1)) + assert IssueQuery.visible(User.find(1)).find_by_id(q.id) + end + + def test_query_with_private_visibility_should_be_visible_to_owner + q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7)) + + assert !q.visible?(User.anonymous) + assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id) + + assert q.visible?(User.find(7)) + assert IssueQuery.visible(User.find(7)).find_by_id(q.id) + + assert !q.visible?(User.find(2)) + assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id) + + assert q.visible?(User.find(1)) + assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id) + end + + test "#available_filters should include users of visible projects in cross-project view" do + users = IssueQuery.new.available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?("3") + end + + test "#available_filters should include users of subprojects" do + user1 = User.generate! + user2 = User.generate! + project = Project.find(1) + Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1]) + + users = IssueQuery.new(:project => project).available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?(user1.id.to_s) + assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s) + end + + test "#available_filters should include visible projects in cross-project view" do + projects = IssueQuery.new.available_filters["project_id"] + assert_not_nil projects + assert projects[:values].map{|u|u[1]}.include?("1") + end + + test "#available_filters should include 'member_of_group' filter" do + query = IssueQuery.new + assert query.available_filters.keys.include?("member_of_group") + assert_equal :list_optional, query.available_filters["member_of_group"][:type] + assert query.available_filters["member_of_group"][:values].present? + assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]}, + query.available_filters["member_of_group"][:values].sort + end + + test "#available_filters should include 'assigned_to_role' filter" do + query = IssueQuery.new + assert query.available_filters.keys.include?("assigned_to_role") + assert_equal :list_optional, query.available_filters["assigned_to_role"][:type] + + assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1']) + assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2']) + assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3']) + + assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4']) + assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5']) + end + + def test_available_filters_should_include_custom_field_according_to_user_visibility + visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true) + hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1]) + + with_current_user User.find(3) do + query = IssueQuery.new + assert_include "cf_#{visible_field.id}", query.available_filters.keys + assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys + end + end + + def test_available_columns_should_include_custom_field_according_to_user_visibility + visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true) + hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1]) + + with_current_user User.find(3) do + query = IssueQuery.new + assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name) + assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name) + end + end + + context "#statement" do + context "with 'member_of_group' filter" do + setup do + Group.destroy_all # No fixtures + @user_in_group = User.generate! + @second_user_in_group = User.generate! + @user_in_group2 = User.generate! + @user_not_in_group = User.generate! + + @group = Group.generate!.reload + @group.users << @user_in_group + @group.users << @second_user_in_group + + @group2 = Group.generate!.reload + @group2.users << @user_in_group2 + + end + + should "search assigned to for users in the group" do + @query = IssueQuery.new(:name => '_') + @query.add_filter('member_of_group', '=', [@group.id.to_s]) + + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search not assigned to any group member (none)" do + @query = IssueQuery.new(:name => '_') + @query.add_filter('member_of_group', '!*', ['']) + + # Users not in a group + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search assigned to any group member (all)" do + @query = IssueQuery.new(:name => '_') + @query.add_filter('member_of_group', '*', ['']) + + # Only users in a group + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "return an empty set with = empty group" do + @empty_group = Group.generate! + @query = IssueQuery.new(:name => '_') + @query.add_filter('member_of_group', '=', [@empty_group.id.to_s]) + + assert_equal [], find_issues_with_query(@query) + end + + should "return issues with ! empty group" do + @empty_group = Group.generate! + @query = IssueQuery.new(:name => '_') + @query.add_filter('member_of_group', '!', [@empty_group.id.to_s]) + + assert_find_issues_with_query_is_successful @query + end + end + + context "with 'assigned_to_role' filter" do + setup do + @manager_role = Role.find_by_name('Manager') + @developer_role = Role.find_by_name('Developer') + + @project = Project.generate! + @manager = User.generate! + @developer = User.generate! + @boss = User.generate! + @guest = User.generate! + User.add_to_project(@manager, @project, @manager_role) + User.add_to_project(@developer, @project, @developer_role) + User.add_to_project(@boss, @project, [@manager_role, @developer_role]) + + @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id) + @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id) + @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id) + @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id) + @issue5 = Issue.generate!(:project => @project) + end + + should "search assigned to for users with the Role" do + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) + + assert_query_result [@issue1, @issue3], @query + end + + should "search assigned to for users with the Role on the issue project" do + other_project = Project.generate! + User.add_to_project(@developer, other_project, @manager_role) + + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) + + assert_query_result [@issue1, @issue3], @query + end + + should "return an empty set with empty role" do + @empty_role = Role.generate! + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s]) + + assert_query_result [], @query + end + + should "search assigned to for users without the Role" do + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s]) + + assert_query_result [@issue2, @issue4, @issue5], @query + end + + should "search assigned to for users not assigned to any Role (none)" do + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '!*', ['']) + + assert_query_result [@issue4, @issue5], @query + end + + should "search assigned to for users assigned to any Role (all)" do + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '*', ['']) + + assert_query_result [@issue1, @issue2, @issue3], @query + end + + should "return issues with ! empty role" do + @empty_role = Role.generate! + @query = IssueQuery.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s]) + + assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25955007822b3924f8cfe4c9f332b3be0a33dd6f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25955007822b3924f8cfe4c9f332b3be0a33dd6f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,61 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ConfigurationTest < ActiveSupport::TestCase + def setup + @conf = Redmine::Configuration + end + + def test_empty + assert_kind_of Hash, load_conf('empty.yml', 'test') + end + + def test_default + assert_kind_of Hash, load_conf('default.yml', 'test') + assert_equal 'foo', @conf['somesetting'] + end + + def test_no_default + assert_kind_of Hash, load_conf('no_default.yml', 'test') + assert_equal 'foo', @conf['somesetting'] + end + + def test_overrides + assert_kind_of Hash, load_conf('overrides.yml', 'test') + assert_equal 'bar', @conf['somesetting'] + end + + def test_with + load_conf('default.yml', 'test') + assert_equal 'foo', @conf['somesetting'] + @conf.with 'somesetting' => 'bar' do + assert_equal 'bar', @conf['somesetting'] + end + assert_equal 'foo', @conf['somesetting'] + end + + private + + def load_conf(file, env) + @conf.load( + :file => File.join(Rails.root, 'test', 'fixtures', 'configuration', file), + :env => env + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25969a217fc5385c0e7df92d761b2fee83db869b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25969a217fc5385c0e7df92d761b2fee83db869b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,42 @@ +/.project +/.loadpath +/.powrc +/.rvmrc +/config/additional_environment.rb +/config/configuration.yml +/config/database.yml +/config/email.yml +/config/initializers/session_store.rb +/config/initializers/secret_token.rb +/coverage +/db/*.db +/db/*.sqlite3 +/db/schema.rb +/files/* +/lib/redmine/scm/adapters/mercurial/redminehelper.pyc +/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/* +/tmp/sessions/* +/tmp/sockets/* +/tmp/test/* +/tmp/thumbnails/* +/vendor/cache +/vendor/rails +*.rbc + +/.bundle +/Gemfile.lock +/Gemfile.local + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25a75439387b5e7c9157d74c8baba2d41c8319c0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25a75439387b5e7c9157d74c8baba2d41c8319c0.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,48 @@ +
    +<%= link_to l(:button_log_time), + {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, + :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %> +
    + +<%= render_timelog_breadcrumb %> + +

    <%= l(:label_spent_time) %>

    + +<%= form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %> +<%= render :partial => 'date_range' %> +<% end %> + +
    +

    <%= l(:label_total_time) %>: <%= html_hours(l_hours(@total_hours)) %>

    +
    + +<% unless @entries.empty? %> +<%= render :partial => 'list', :locals => { :entries => @entries }%> +

    <%= pagination_links_full @entry_pages, @entry_count %>

    + +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %> + <%= f.link_to 'CSV', :url => params, :onclick => "showModal('csv-export-options', '330px'); return false;" %> +<% end %> + + +<% end %> + +<% html_title l(:label_spent_time), l(:label_details) %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25a7a4fdea85fbb1b14478fb690111974ca9c5c6.svn-base --- a/.svn/pristine/25/25a7a4fdea85fbb1b14478fb690111974ca9c5c6.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 DocumentCategory < Enumeration - has_many :documents, :foreign_key => 'category_id' - - OptionName = :enumeration_doc_categories - - def option_name - OptionName - end - - def objects_count - documents.count - end - - def transfer_relations(to) - documents.update_all("category_id = #{to.id}") - end - - def self.default - d = super - d = first if d.nil? - d - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25cdcad944ee39e6a9c2a56e5a525014ecebc7bc.svn-base --- a/.svn/pristine/25/25cdcad944ee39e6a9c2a56e5a525014ecebc7bc.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,525 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RepositoriesMercurialControllerTest < ActionController::TestCase - tests RepositoriesController - - fixtures :projects, :users, :roles, :members, :member_roles, - :repositories, :enabled_modules - - REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s - CHAR_1_HEX = "\xc3\x9c" - PRJ_ID = 3 - NUM_REV = 32 - - ruby19_non_utf8_pass = - (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8') - - def setup - User.current = nil - @project = Project.find(PRJ_ID) - @repository = Repository::Mercurial.create( - :project => @project, - :url => REPOSITORY_PATH, - :path_encoding => 'ISO-8859-1' - ) - assert @repository - @diff_c_support = true - @char_1 = CHAR_1_HEX.dup - @tag_char_1 = "tag-#{CHAR_1_HEX}-00" - @branch_char_0 = "branch-#{CHAR_1_HEX}-00" - @branch_char_1 = "branch-#{CHAR_1_HEX}-01" - if @char_1.respond_to?(:force_encoding) - @char_1.force_encoding('UTF-8') - @tag_char_1.force_encoding('UTF-8') - @branch_char_0.force_encoding('UTF-8') - @branch_char_1.force_encoding('UTF-8') - end - end - - if ruby19_non_utf8_pass - puts "TODO: Mercurial functional test fails in Ruby 1.9 " + - "and Encoding.default_external is not UTF-8. " + - "Current value is '#{Encoding.default_external.to_s}'" - def test_fake; assert true end - elsif File.directory?(REPOSITORY_PATH) - - def test_get_new - @request.session[:user_id] = 1 - @project.repository.destroy - get :new, :project_id => 'subproject1', :repository_scm => 'Mercurial' - assert_response :success - assert_template 'new' - assert_kind_of Repository::Mercurial, assigns(:repository) - assert assigns(:repository).new_record? - end - - def test_show_root - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - get :show, :id => PRJ_ID - assert_response :success - assert_template 'show' - assert_not_nil assigns(:entries) - assert_equal 4, assigns(:entries).size - assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} - assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} - assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} - assert_not_nil assigns(:changesets) - assert assigns(:changesets).size > 0 - end - - def test_show_directory - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] - assert_response :success - assert_template 'show' - assert_not_nil assigns(:entries) - assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) - entry = assigns(:entries).detect {|e| e.name == 'edit.png'} - assert_not_nil entry - assert_equal 'file', entry.kind - assert_equal 'images/edit.png', entry.path - assert_not_nil assigns(:changesets) - assert assigns(:changesets).size > 0 - end - - def test_show_at_given_revision - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [0, '0', '0885933ad4f6'].each do |r1| - get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], - :rev => r1 - assert_response :success - assert_template 'show' - assert_not_nil assigns(:entries) - assert_equal ['delete.png'], assigns(:entries).collect(&:name) - assert_not_nil assigns(:changesets) - assert assigns(:changesets).size > 0 - end - end - - def test_show_directory_sql_escape_percent - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [13, '13', '3a330eb32958'].each do |r1| - get :show, :id => PRJ_ID, - :path => repository_path_hash(['sql_escape', 'percent%dir'])[:param], - :rev => r1 - assert_response :success - assert_template 'show' - - assert_not_nil assigns(:entries) - assert_equal ['percent%file1.txt', 'percentfile1.txt'], - assigns(:entries).collect(&:name) - changesets = assigns(:changesets) - assert_not_nil changesets - assert assigns(:changesets).size > 0 - assert_equal %w(13 11 10 9), changesets.collect(&:revision) - end - end - - def test_show_directory_latin_1_path - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [21, '21', 'adf805632193'].each do |r1| - get :show, :id => PRJ_ID, - :path => repository_path_hash(['latin-1-dir'])[:param], - :rev => r1 - assert_response :success - assert_template 'show' - - assert_not_nil assigns(:entries) - assert_equal ["make-latin-1-file.rb", - "test-#{@char_1}-1.txt", - "test-#{@char_1}-2.txt", - "test-#{@char_1}.txt"], assigns(:entries).collect(&:name) - changesets = assigns(:changesets) - assert_not_nil changesets - assert_equal %w(21 20 19 18 17), changesets.collect(&:revision) - end - end - - def show_should_show_branch_selection_form - @repository.fetch_changesets - @project.reload - get :show, :id => PRJ_ID - assert_tag 'form', :attributes => {:id => 'revision_selector', :action => '/projects/subproject1/repository/show'} - assert_tag 'select', :attributes => {:name => 'branch'}, - :child => {:tag => 'option', :attributes => {:value => 'test-branch-01'}}, - :parent => {:tag => 'form', :attributes => {:id => 'revision_selector'}} - end - - def test_show_branch - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [ - 'default', - @branch_char_1, - 'branch (1)[2]&,%.-3_4', - @branch_char_0, - 'test_branch.latin-1', - 'test-branch-00', - ].each do |bra| - get :show, :id => PRJ_ID, :rev => bra - assert_response :success - assert_template 'show' - assert_not_nil assigns(:entries) - assert assigns(:entries).size > 0 - assert_not_nil assigns(:changesets) - assert assigns(:changesets).size > 0 - end - end - - def test_show_tag - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [ - @tag_char_1, - 'tag_test.00', - 'tag-init-revision' - ].each do |tag| - get :show, :id => PRJ_ID, :rev => tag - assert_response :success - assert_template 'show' - assert_not_nil assigns(:entries) - assert assigns(:entries).size > 0 - assert_not_nil assigns(:changesets) - assert assigns(:changesets).size > 0 - end - end - - def test_changes - get :changes, :id => PRJ_ID, - :path => repository_path_hash(['images', 'edit.png'])[:param] - assert_response :success - assert_template 'changes' - assert_tag :tag => 'h2', :content => 'edit.png' - end - - def test_entry_show - get :entry, :id => PRJ_ID, - :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] - assert_response :success - assert_template 'entry' - # Line 10 - assert_tag :tag => 'th', - :content => '10', - :attributes => { :class => 'line-num' }, - :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } - end - - def test_entry_show_latin_1_path - [21, '21', 'adf805632193'].each do |r1| - get :entry, :id => PRJ_ID, - :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param], - :rev => r1 - assert_response :success - assert_template 'entry' - assert_tag :tag => 'th', - :content => '1', - :attributes => { :class => 'line-num' }, - :sibling => { :tag => 'td', - :content => /Mercurial is a distributed version control system/ } - end - end - - def test_entry_show_latin_1_contents - with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do - [27, '27', '7bbf4c738e71'].each do |r1| - get :entry, :id => PRJ_ID, - :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], - :rev => r1 - assert_response :success - assert_template 'entry' - assert_tag :tag => 'th', - :content => '1', - :attributes => { :class => 'line-num' }, - :sibling => { :tag => 'td', - :content => /test-#{@char_1}.txt/ } - end - end - end - - def test_entry_download - get :entry, :id => PRJ_ID, - :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], - :format => 'raw' - assert_response :success - # File content - assert @response.body.include?('WITHOUT ANY WARRANTY') - end - - def test_entry_binary_force_download - get :entry, :id => PRJ_ID, :rev => 1, - :path => repository_path_hash(['images', 'edit.png'])[:param] - assert_response :success - assert_equal 'image/png', @response.content_type - end - - def test_directory_entry - get :entry, :id => PRJ_ID, - :path => repository_path_hash(['sources'])[:param] - assert_response :success - assert_template 'show' - assert_not_nil assigns(:entry) - assert_equal 'sources', assigns(:entry).name - end - - def test_diff - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [4, '4', 'def6d2f1254a'].each do |r1| - # Full diff of changeset 4 - ['inline', 'sbs'].each do |dt| - get :diff, :id => PRJ_ID, :rev => r1, :type => dt - assert_response :success - assert_template 'diff' - if @diff_c_support - # Line 22 removed - assert_tag :tag => 'th', - :content => '22', - :sibling => { :tag => 'td', - :attributes => { :class => /diff_out/ }, - :content => /def remove/ } - assert_tag :tag => 'h2', :content => /4:def6d2f1254a/ - end - end - end - end - - def test_diff_two_revs - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [2, '400bb8672109', '400', 400].each do |r1| - [4, 'def6d2f1254a'].each do |r2| - ['inline', 'sbs'].each do |dt| - get :diff, - :id => PRJ_ID, - :rev => r1, - :rev_to => r2, - :type => dt - assert_response :success - assert_template 'diff' - diff = assigns(:diff) - assert_not_nil diff - assert_tag :tag => 'h2', - :content => /4:def6d2f1254a 2:400bb8672109/ - end - end - end - end - - def test_diff_latin_1_path - with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do - [21, 'adf805632193'].each do |r1| - ['inline', 'sbs'].each do |dt| - get :diff, :id => PRJ_ID, :rev => r1, :type => dt - assert_response :success - assert_template 'diff' - assert_tag :tag => 'thead', - :descendant => { - :tag => 'th', - :attributes => { :class => 'filename' } , - :content => /latin-1-dir\/test-#{@char_1}-2.txt/ , - }, - :sibling => { - :tag => 'tbody', - :descendant => { - :tag => 'td', - :attributes => { :class => /diff_in/ }, - :content => /It is written in Python/ - } - } - end - end - end - end - - def test_diff_should_show_modified_filenames - get :diff, :id => PRJ_ID, :rev => '400bb8672109', :type => 'inline' - assert_response :success - assert_template 'diff' - assert_select 'th.filename', :text => 'sources/watchers_controller.rb' - end - - def test_diff_should_show_deleted_filenames - get :diff, :id => PRJ_ID, :rev => 'b3a615152df8', :type => 'inline' - assert_response :success - assert_template 'diff' - assert_select 'th.filename', :text => 'sources/welcome_controller.rb' - end - - def test_annotate - get :annotate, :id => PRJ_ID, - :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] - assert_response :success - assert_template 'annotate' - - # Line 22, revision 4:def6d2f1254a - assert_select 'tr' do - assert_select 'th.line-num', :text => '22' - assert_select 'td.revision', :text => '4:def6d2f1254a' - assert_select 'td.author', :text => 'jsmith' - assert_select 'td', :text => /remove_watcher/ - end - end - - def test_annotate_not_in_tip - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - get :annotate, :id => PRJ_ID, - :path => repository_path_hash(['sources', 'welcome_controller.rb'])[:param] - assert_response 404 - assert_error_tag :content => /was not found/ - end - - def test_annotate_at_given_revision - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - [2, '400bb8672109', '400', 400].each do |r1| - get :annotate, :id => PRJ_ID, :rev => r1, - :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] - assert_response :success - assert_template 'annotate' - assert_tag :tag => 'h2', :content => /@ 2:400bb8672109/ - end - end - - def test_annotate_latin_1_path - [21, '21', 'adf805632193'].each do |r1| - get :annotate, :id => PRJ_ID, - :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param], - :rev => r1 - assert_response :success - assert_template 'annotate' - assert_tag :tag => 'th', - :content => '1', - :attributes => { :class => 'line-num' }, - :sibling => - { - :tag => 'td', - :attributes => { :class => 'revision' }, - :child => { :tag => 'a', :content => '20:709858aafd1b' } - } - assert_tag :tag => 'th', - :content => '1', - :attributes => { :class => 'line-num' }, - :sibling => - { - :tag => 'td' , - :content => 'jsmith' , - :attributes => { :class => 'author' }, - } - assert_tag :tag => 'th', - :content => '1', - :attributes => { :class => 'line-num' }, - :sibling => { :tag => 'td', - :content => /Mercurial is a distributed version control system/ } - - end - end - - def test_annotate_latin_1_contents - with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do - [27, '7bbf4c738e71'].each do |r1| - get :annotate, :id => PRJ_ID, - :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], - :rev => r1 - assert_tag :tag => 'th', - :content => '1', - :attributes => { :class => 'line-num' }, - :sibling => { :tag => 'td', - :content => /test-#{@char_1}.txt/ } - end - end - end - - def test_empty_revision - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - @project.reload - assert_equal NUM_REV, @repository.changesets.count - ['', ' ', nil].each do |r| - get :revision, :id => PRJ_ID, :rev => r - assert_response 404 - assert_error_tag :content => /was not found/ - end - end - - def test_destroy_valid_repository - @request.session[:user_id] = 1 # admin - assert_equal 0, @repository.changesets.count - @repository.fetch_changesets - assert_equal NUM_REV, @repository.changesets.count - - assert_difference 'Repository.count', -1 do - delete :destroy, :id => @repository.id - end - assert_response 302 - @project.reload - assert_nil @project.repository - end - - def test_destroy_invalid_repository - @request.session[:user_id] = 1 # admin - @project.repository.destroy - @repository = Repository::Mercurial.create!( - :project => Project.find(PRJ_ID), - :url => "/invalid", - :path_encoding => 'ISO-8859-1' - ) - @repository.fetch_changesets - assert_equal 0, @repository.changesets.count - - assert_difference 'Repository.count', -1 do - delete :destroy, :id => @repository.id - end - assert_response 302 - @project.reload - assert_nil @project.repository - end - else - puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!" - def test_fake; assert true end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25da0d3c14f76d4d61d0a700c479d9e89afb5581.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25da0d3c14f76d4d61d0a700c479d9e89afb5581.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,42 @@ +<%= form_tag(project_enumerations_path(@project), :method => :put, :class => "tabular") do %> + + + + + + <% TimeEntryActivity.new.available_custom_fields.each do |value| %> + + <% end %> + + + + <% @project.activities(true).each do |enumeration| %> + <%= fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %> + + + + <% enumeration.custom_field_values.each do |value| %> + + <% end %> + + + <% end %> + <% end %> +
    <%= l(:field_name) %><%= l(:enumeration_system_activity) %><%= h value.name %><%= l(:field_active) %>
    + <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %> + <%= h(enumeration) %> + <%= checked_image !enumeration.project %> + <%= custom_field_tag "enumerations[#{enumeration.id}]", value %> + + <%= ff.check_box :active %> +
    + +
    +<%= link_to(l(:button_reset), project_enumerations_path(@project), + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del') %> +
    + +<%= submit_tag l(:button_save) %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25dd827654fa8266aba700a8953d3bb722e81865.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25dd827654fa8266aba700a8953d3bb722e81865.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,247 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 WikiFormatting + module Macros + module Definitions + # Returns true if +name+ is the name of an existing macro + def macro_exists?(name) + Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym) + end + + def exec_macro(name, obj, args, text) + macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym] + return unless macro_options + + method_name = "macro_#{name}" + unless macro_options[:parse_args] == false + args = args.split(',').map(&:strip) + end + + begin + if self.class.instance_method(method_name).arity == 3 + send(method_name, obj, args, text) + elsif text + raise "This macro does not accept a block of text" + else + send(method_name, obj, args) + end + rescue => e + "
    Error executing the #{h name} macro (#{h e.to_s})
    ".html_safe + end + end + + def extract_macro_options(args, *keys) + options = {} + while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym) + options[$1.downcase.to_sym] = $2 + args.pop + end + return [args, options] + end + end + + @@available_macros = {} + mattr_accessor :available_macros + + class << self + # Plugins can use this method to define new macros: + # + # Redmine::WikiFormatting::Macros.register do + # desc "This is my macro" + # macro :my_macro do |obj, args| + # "My macro output" + # end + # + # desc "This is my macro that accepts a block of text" + # macro :my_macro do |obj, args, text| + # "My macro output" + # end + # end + def register(&block) + class_eval(&block) if block_given? + end + + # Defines a new macro with the given name, options and block. + # + # Options: + # * :desc - A description of the macro + # * :parse_args => false - Disables arguments parsing (the whole arguments + # string is passed to the macro) + # + # Macro blocks accept 2 or 3 arguments: + # * obj: the object that is rendered (eg. an Issue, a WikiContent...) + # * args: macro arguments + # * text: the block of text given to the macro (should be present only if the + # macro accepts a block of text). text is a String or nil if the macro is + # invoked without a block of text. + # + # Examples: + # By default, when the macro is invoked, the coma separated list of arguments + # is split and passed to the macro block as an array. If no argument is given + # the macro will be invoked with an empty array: + # + # macro :my_macro do |obj, args| + # # args is an array + # # and this macro do not accept a block of text + # end + # + # You can disable arguments spliting with the :parse_args => false option. In + # this case, the full string of arguments is passed to the macro: + # + # macro :my_macro, :parse_args => false do |obj, args| + # # args is a string + # end + # + # Macro can optionally accept a block of text: + # + # macro :my_macro do |obj, args, text| + # # this macro accepts a block of text + # end + # + # Macros are invoked in formatted text using double curly brackets. Arguments + # must be enclosed in parenthesis if any. A new line after the macro name or the + # arguments starts the block of text that will be passe to the macro (invoking + # a macro that do not accept a block of text with some text will fail). + # Examples: + # + # No arguments: + # {{my_macro}} + # + # With arguments: + # {{my_macro(arg1, arg2)}} + # + # With a block of text: + # {{my_macro + # multiple lines + # of text + # }} + # + # With arguments and a block of text + # {{my_macro(arg1, arg2) + # multiple lines + # of text + # }} + # + # If a block of text is given, the closing tag }} must be at the start of a new line. + def macro(name, options={}, &block) + options.assert_valid_keys(:desc, :parse_args) + unless name.to_s.match(/\A\w+\z/) + raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)" + end + unless block_given? + raise "Can not create a macro without a block!" + end + name = name.to_s.downcase.to_sym + available_macros[name] = {:desc => @@desc || ''}.merge(options) + @@desc = nil + Definitions.send :define_method, "macro_#{name}", &block + end + + # Sets description for the next macro to be defined + def desc(txt) + @@desc = txt + end + end + + # Builtin macros + desc "Sample macro." + macro :hello_world do |obj, args, text| + h("Hello world! Object: #{obj.class.name}, " + + (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") + + " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.") + ) + end + + desc "Displays a list of all available macros, including description if available." + macro :macro_list do |obj, args| + out = ''.html_safe + @@available_macros.each do |macro, options| + out << content_tag('dt', content_tag('code', macro.to_s)) + out << content_tag('dd', textilizable(options[:desc])) + end + content_tag('dl', out) + end + + desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" + + " !{{child_pages}} -- can be used from a wiki page only\n" + + " !{{child_pages(depth=2)}} -- display 2 levels nesting only\n" + " !{{child_pages(Foo)}} -- lists all children of page Foo\n" + + " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo" + macro :child_pages do |obj, args| + args, options = extract_macro_options(args, :parent, :depth) + options[:depth] = options[:depth].to_i if options[:depth].present? + + page = nil + if args.size > 0 + page = Wiki.find_page(args.first.to_s, :project => @project) + elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version) + page = obj.page + else + raise 'With no argument, this macro can be called from wiki pages only.' + end + raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) + pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id) + render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id) + end + + desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}" + macro :include do |obj, args| + page = Wiki.find_page(args.first.to_s, :project => @project) + raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) + @included_wiki_pages ||= [] + raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) + @included_wiki_pages << page.title + out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false) + @included_wiki_pages.pop + out + end + + desc "Inserts of collapsed block of text. Example:\n\n {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}" + macro :collapse do |obj, args, text| + html_id = "collapse-#{Redmine::Utils.random_hex(4)}" + show_label = args[0] || l(:button_show) + hide_label = args[1] || args[0] || l(:button_hide) + js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);" + out = ''.html_safe + out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed') + out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;') + out << content_tag('div', textilizable(text, :object => obj), :id => html_id, :class => 'collapsed-text', :style => 'display:none;') + out + end + + desc "Displays a clickable thumbnail of an attached image. Examples:\n\n
    {{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}
    " + macro :thumbnail do |obj, args| + args, options = extract_macro_options(args, :size, :title) + filename = args.first + raise 'Filename required' unless filename.present? + size = options[:size] + raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/) + size = size.to_i + size = nil unless size > 0 + if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename) + title = options[:title] || attachment.title + img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename) + link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title) + else + raise "Attachment #{filename} not found" + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/25/25e1449caab60bc909f79da5e84ca8f30d8566d5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25e1449caab60bc909f79da5e84ca8f30d8566d5.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,47 @@ +<%= labelled_fields_for :issue, @issue do |f| %> +<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %> + +<% if @issue.safe_attribute? 'is_private' %> +

    + <%= f.check_box :is_private, :no_label => true %> +

    +<% end %> + +<% if @issue.safe_attribute? 'project_id' %> +

    <%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %>

    +<% end %> + +<% if @issue.safe_attribute? 'tracker_id' %> +

    <%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %>

    +<% end %> + +<% if @issue.safe_attribute? 'subject' %> +

    <%= f.text_field :subject, :size => 80, :maxlength => 255, :required => true %>

    +<% end %> + +<% if @issue.safe_attribute? 'description' %> +

    + <%= f.label_for_field :description, :required => @issue.required_attribute?('description') %> + <%= link_to_function image_tag('edit.png'), '$(this).hide(); $("#issue_description_and_toolbar").show()' unless @issue.new_record? %> + <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %> + <%= f.text_area :description, + :cols => 60, + :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), + :accesskey => accesskey(:edit), + :class => 'wiki-edit', + :no_label => true %> + <% end %> +

    +<%= wikitoolbar_for 'issue_description' %> +<% end %> + +
    + <%= render :partial => 'issues/attributes' %> +
    + +<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> +<% end %> + +<% heads_for_wiki_formatter %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/2610f5493d7250662d4298685db33ab62ac8d21f.svn-base --- a/.svn/pristine/26/2610f5493d7250662d4298685db33ab62ac8d21f.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,44 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ApiTest::EnumerationsTest < ActionController::IntegrationTest - fixtures :enumerations - - def setup - Setting.rest_api_enabled = '1' - end - - context "/enumerations/issue_priorities" do - context "GET" do - - should "return priorities" do - get '/enumerations/issue_priorities.xml' - - assert_response :success - assert_equal 'application/xml', response.content_type - assert_select 'issue_priorities[type=array]' do - assert_select 'issue_priority' do - assert_select 'id', :text => '6' - assert_select 'name', :text => 'High' - end - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/261610bb580a68c8ae61110e8a808bb695edda19.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/261610bb580a68c8ae61110e8a808bb695edda19.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,475 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 #:nodoc: + + class PluginNotFound < StandardError; end + class PluginRequirementError < StandardError; end + + # Base class for Redmine plugins. + # Plugins are registered using the register class method that acts as the public constructor. + # + # Redmine::Plugin.register :example do + # name 'Example plugin' + # author 'John Smith' + # description 'This is an example plugin for Redmine' + # version '0.0.1' + # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings' + # end + # + # === Plugin attributes + # + # +settings+ is an optional attribute that let the plugin be configurable. + # It must be a hash with the following keys: + # * :default: default value for the plugin settings + # * :partial: path of the configuration partial view, relative to the plugin app/views directory + # Example: + # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings' + # In this example, the settings partial will be found here in the plugin directory: app/views/settings/_settings.rhtml. + # + # When rendered, the plugin settings value is available as the local variable +settings+ + class Plugin + cattr_accessor :directory + self.directory = File.join(Rails.root, 'plugins') + + cattr_accessor :public_directory + self.public_directory = File.join(Rails.root, 'public', 'plugin_assets') + + @registered_plugins = {} + class << self + attr_reader :registered_plugins + private :new + + def def_field(*names) + class_eval do + names.each do |name| + define_method(name) do |*args| + args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args) + end + end + end + end + end + def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory + attr_reader :id + + # Plugin constructor + def self.register(id, &block) + p = new(id) + p.instance_eval(&block) + + # Set a default name if it was not provided during registration + p.name(id.to_s.humanize) if p.name.nil? + # Set a default directory if it was not provided during registration + p.directory(File.join(self.directory, id.to_s)) if p.directory.nil? + + # Adds plugin locales if any + # YAML translation files should be found under /config/locales/ + ::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml')) + + # Prepends the app/views directory of the plugin to the view path + view_path = File.join(p.directory, 'app', 'views') + if File.directory?(view_path) + ActionController::Base.prepend_view_path(view_path) + ActionMailer::Base.prepend_view_path(view_path) + end + + # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path + Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir| + ActiveSupport::Dependencies.autoload_paths += [dir] + end + + registered_plugins[id] = p + end + + # Returns an array of all registered plugins + def self.all + registered_plugins.values.sort + end + + # Finds a plugin by its id + # Returns a PluginNotFound exception if the plugin doesn't exist + def self.find(id) + registered_plugins[id.to_sym] || raise(PluginNotFound) + end + + # Clears the registered plugins hash + # It doesn't unload installed plugins + def self.clear + @registered_plugins = {} + end + + # Checks if a plugin is installed + # + # @param [String] id name of the plugin + def self.installed?(id) + registered_plugins[id.to_sym].present? + end + + def self.load + Dir.glob(File.join(self.directory, '*')).sort.each do |directory| + if File.directory?(directory) + lib = File.join(directory, "lib") + if File.directory?(lib) + $:.unshift lib + ActiveSupport::Dependencies.autoload_paths += [lib] + end + initializer = File.join(directory, "init.rb") + if File.file?(initializer) + require initializer + end + end + end + end + + def initialize(id) + @id = id.to_sym + end + + def public_directory + File.join(self.class.public_directory, id.to_s) + end + + def to_param + id + end + + def assets_directory + File.join(directory, 'assets') + end + + def <=>(plugin) + self.id.to_s <=> plugin.id.to_s + end + + # Sets a requirement on Redmine version + # Raises a PluginRequirementError exception if the requirement is not met + # + # Examples + # # Requires Redmine 0.7.3 or higher + # requires_redmine :version_or_higher => '0.7.3' + # requires_redmine '0.7.3' + # + # # Requires Redmine 0.7.x or higher + # requires_redmine '0.7' + # + # # Requires a specific Redmine version + # requires_redmine :version => '0.7.3' # 0.7.3 only + # requires_redmine :version => '0.7' # 0.7.x + # requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0 + # + # # Requires a Redmine version within a range + # requires_redmine :version => '0.7.3'..'0.9.1' # >= 0.7.3 and <= 0.9.1 + # requires_redmine :version => '0.7'..'0.9' # >= 0.7.x and <= 0.9.x + def requires_redmine(arg) + arg = { :version_or_higher => arg } unless arg.is_a?(Hash) + arg.assert_valid_keys(:version, :version_or_higher) + + current = Redmine::VERSION.to_a + arg.each do |k, req| + case k + when :version_or_higher + raise ArgumentError.new(":version_or_higher accepts a version string only") unless req.is_a?(String) + unless compare_versions(req, current) <= 0 + raise PluginRequirementError.new("#{id} plugin requires Redmine #{req} or higher but current is #{current.join('.')}") + end + when :version + req = [req] if req.is_a?(String) + if req.is_a?(Array) + unless req.detect {|ver| compare_versions(ver, current) == 0} + raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{req.join(', ')} but current is #{current.join('.')}") + end + elsif req.is_a?(Range) + unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 + raise PluginRequirementError.new("#{id} plugin requires a Redmine version between #{req.first} and #{req.last} but current is #{current.join('.')}") + end + else + raise ArgumentError.new(":version option accepts a version string, an array or a range of versions") + end + end + end + true + end + + def compare_versions(requirement, current) + requirement = requirement.split('.').collect(&:to_i) + requirement <=> current.slice(0, requirement.size) + end + private :compare_versions + + # Sets a requirement on a Redmine plugin version + # Raises a PluginRequirementError exception if the requirement is not met + # + # Examples + # # Requires a plugin named :foo version 0.7.3 or higher + # requires_redmine_plugin :foo, :version_or_higher => '0.7.3' + # requires_redmine_plugin :foo, '0.7.3' + # + # # Requires a specific version of a Redmine plugin + # requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only + # requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0 + def requires_redmine_plugin(plugin_name, arg) + arg = { :version_or_higher => arg } unless arg.is_a?(Hash) + arg.assert_valid_keys(:version, :version_or_higher) + + plugin = Plugin.find(plugin_name) + current = plugin.version.split('.').collect(&:to_i) + + arg.each do |k, v| + v = [] << v unless v.is_a?(Array) + versions = v.collect {|s| s.split('.').collect(&:to_i)} + case k + when :version_or_higher + raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1 + unless (current <=> versions.first) >= 0 + raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}") + end + when :version + unless versions.include?(current.slice(0,3)) + raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}") + end + end + end + true + end + + # Adds an item to the given +menu+. + # The +id+ parameter (equals to the project id) is automatically added to the url. + # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample' + # + # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu + # + def menu(menu, item, url, options={}) + Redmine::MenuManager.map(menu).push(item, url, options) + end + alias :add_menu_item :menu + + # Removes +item+ from the given +menu+. + def delete_menu_item(menu, item) + Redmine::MenuManager.map(menu).delete(item) + end + + # Defines a permission called +name+ for the given +actions+. + # + # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array): + # permission :destroy_contacts, { :contacts => :destroy } + # permission :view_contacts, { :contacts => [:index, :show] } + # + # The +options+ argument is a hash that accept the following keys: + # * :public => the permission is public if set to true (implicitly given to any user) + # * :require => can be set to one of the following values to restrict users the permission can be given to: :loggedin, :member + # * :read => set it to true so that the permission is still granted on closed projects + # + # Examples + # # A permission that is implicitly given to any user + # # This permission won't appear on the Roles & Permissions setup screen + # permission :say_hello, { :example => :say_hello }, :public => true, :read => true + # + # # A permission that can be given to any user + # permission :say_hello, { :example => :say_hello } + # + # # A permission that can be given to registered users only + # permission :say_hello, { :example => :say_hello }, :require => :loggedin + # + # # A permission that can be given to project members only + # permission :say_hello, { :example => :say_hello }, :require => :member + def permission(name, actions, options = {}) + if @project_module + Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}} + else + Redmine::AccessControl.map {|map| map.permission(name, actions, options)} + end + end + + # Defines a project module, that can be enabled/disabled for each project. + # Permissions defined inside +block+ will be bind to the module. + # + # project_module :things do + # permission :view_contacts, { :contacts => [:list, :show] }, :public => true + # permission :destroy_contacts, { :contacts => :destroy } + # end + def project_module(name, &block) + @project_module = name + self.instance_eval(&block) + @project_module = nil + end + + # Registers an activity provider. + # + # Options: + # * :class_name - one or more model(s) that provide these events (inferred from event_type by default) + # * :default - setting this option to false will make the events not displayed by default + # + # A model can provide several activity event types. + # + # Examples: + # register :news + # register :scrums, :class_name => 'Meeting' + # register :issues, :class_name => ['Issue', 'Journal'] + # + # Retrieving events: + # Associated model(s) must implement the find_events class method. + # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method. + # + # The following call should return all the scrum events visible by current user that occured in the 5 last days: + # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today) + # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only + # + # Note that :view_scrums permission is required to view these events in the activity view. + def activity_provider(*args) + Redmine::Activity.register(*args) + end + + # Registers a wiki formatter. + # + # Parameters: + # * +name+ - human-readable name + # * +formatter+ - formatter class, which should have an instance method +to_html+ + # * +helper+ - helper module, which will be included by wiki pages + def wiki_format_provider(name, formatter, helper) + Redmine::WikiFormatting.register(name, formatter, helper) + end + + # Returns +true+ if the plugin can be configured. + def configurable? + settings && settings.is_a?(Hash) && !settings[:partial].blank? + end + + def mirror_assets + source = assets_directory + destination = public_directory + return unless File.directory?(source) + + source_files = Dir[source + "/**/*"] + source_dirs = source_files.select { |d| File.directory?(d) } + source_files -= source_dirs + + unless source_files.empty? + base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, '')) + begin + FileUtils.mkdir_p(base_target_dir) + rescue Exception => e + raise "Could not create directory #{base_target_dir}: " + e.message + end + end + + source_dirs.each do |dir| + # strip down these paths so we have simple, relative paths we can + # add to the destination + target_dir = File.join(destination, dir.gsub(source, '')) + begin + FileUtils.mkdir_p(target_dir) + rescue Exception => e + raise "Could not create directory #{target_dir}: " + e.message + end + end + + source_files.each do |file| + begin + target = File.join(destination, file.gsub(source, '')) + unless File.exist?(target) && FileUtils.identical?(file, target) + FileUtils.cp(file, target) + end + rescue Exception => e + raise "Could not copy #{file} to #{target}: " + e.message + end + end + end + + # Mirrors assets from one or all plugins to public/plugin_assets + def self.mirror_assets(name=nil) + if name.present? + find(name).mirror_assets + else + all.each do |plugin| + plugin.mirror_assets + end + end + end + + # The directory containing this plugin's migrations (plugin/db/migrate) + def migration_directory + File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate') + end + + # Returns the version number of the latest migration for this plugin. Returns + # nil if this plugin has no migrations. + def latest_migration + migrations.last + end + + # Returns the version numbers of all migrations for this plugin. + def migrations + migrations = Dir[migration_directory+"/*.rb"] + migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort + end + + # Migrate this plugin to the given version + def migrate(version = nil) + puts "Migrating #{id} (#{name})..." + Redmine::Plugin::Migrator.migrate_plugin(self, version) + end + + # Migrates all plugins or a single plugin to a given version + # Exemples: + # Plugin.migrate + # Plugin.migrate('sample_plugin') + # Plugin.migrate('sample_plugin', 1) + # + def self.migrate(name=nil, version=nil) + if name.present? + find(name).migrate(version) + else + all.each do |plugin| + plugin.migrate + end + end + end + + class Migrator < ActiveRecord::Migrator + # We need to be able to set the 'current' plugin being migrated. + cattr_accessor :current_plugin + + class << self + # Runs the migrations from a plugin, up (or down) to the version given + def migrate_plugin(plugin, version) + self.current_plugin = plugin + return if current_version(plugin) == version + migrate(plugin.migration_directory, version) + end + + def current_version(plugin=current_plugin) + # Delete migrations that don't match .. to_i will work because the number comes first + ::ActiveRecord::Base.connection.select_values( + "SELECT version FROM #{schema_migrations_table_name}" + ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0 + end + end + + def migrated + sm_table = self.class.schema_migrations_table_name + ::ActiveRecord::Base.connection.select_values( + "SELECT version FROM #{sm_table}" + ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort + end + + def record_version_state_after_migrating(version) + super(version.to_s + "-" + current_plugin.id.to_s) + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/263797043f9a766a1d04d05671c734ccabf95fbd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/263797043f9a766a1d04d05671c734ccabf95fbd.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,264 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 SearchControllerTest < ActionController::TestCase + fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles, + :issues, :trackers, :issue_statuses, :enumerations, + :custom_fields, :custom_values, + :repositories, :changesets + + def setup + User.current = nil + end + + def test_search_for_projects + get :index + assert_response :success + assert_template 'index' + + get :index, :q => "cook" + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Project.find(1)) + end + + def test_search_all_projects + get :index, :q => 'recipe subproject commit', :all_words => '' + assert_response :success + assert_template 'index' + + assert assigns(:results).include?(Issue.find(2)) + assert assigns(:results).include?(Issue.find(5)) + assert assigns(:results).include?(Changeset.find(101)) + assert_tag :dt, :attributes => { :class => /issue/ }, + :child => { :tag => 'a', :content => /Add ingredients categories/ }, + :sibling => { :tag => 'dd', :content => /should be classified by categories/ } + + assert assigns(:results_by_type).is_a?(Hash) + assert_equal 5, assigns(:results_by_type)['changesets'] + assert_tag :a, :content => 'Changesets (5)' + end + + def test_search_issues + get :index, :q => 'issue', :issues => 1 + assert_response :success + assert_template 'index' + + assert_equal true, assigns(:all_words) + assert_equal false, assigns(:titles_only) + assert assigns(:results).include?(Issue.find(8)) + assert assigns(:results).include?(Issue.find(5)) + assert_tag :dt, :attributes => { :class => /issue closed/ }, + :child => { :tag => 'a', :content => /Closed/ } + end + + def test_search_issues_should_search_notes + Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword') + + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_include Issue.find(2), assigns(:results) + end + + def test_search_issues_with_multiple_matches_in_journals_should_return_issue_once + Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword') + Journal.create!(:journalized => Issue.find(2), :notes => 'Issue notes with searchkeyword') + + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_include Issue.find(2), assigns(:results) + assert_equal 1, assigns(:results).size + end + + def test_search_issues_should_search_private_notes_with_permission_only + Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true) + @request.session[:user_id] = 2 + + Role.find(1).add_permission! :view_private_notes + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_include Issue.find(2), assigns(:results) + + Role.find(1).remove_permission! :view_private_notes + get :index, :q => 'searchkeyword', :issues => 1 + assert_response :success + assert_not_include Issue.find(2), assigns(:results) + end + + def test_search_all_projects_with_scope_param + get :index, :q => 'issue', :scope => 'all' + assert_response :success + assert_template 'index' + assert assigns(:results).present? + end + + def test_search_my_projects + @request.session[:user_id] = 2 + get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => '' + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Issue.find(1)) + assert !assigns(:results).include?(Issue.find(5)) + end + + def test_search_my_projects_without_memberships + # anonymous user has no memberships + get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => '' + assert_response :success + assert_template 'index' + assert assigns(:results).empty? + end + + def test_search_project_and_subprojects + get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => '' + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Issue.find(1)) + assert assigns(:results).include?(Issue.find(5)) + end + + def test_search_without_searchable_custom_fields + CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" + + get :index, :id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:project) + + get :index, :id => 1, :q => "can" + assert_response :success + assert_template 'index' + end + + def test_search_with_searchable_custom_fields + get :index, :id => 1, :q => "stringforcustomfield" + assert_response :success + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(7)) + end + + def test_search_all_words + # 'all words' is on by default + get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1' + assert_equal true, assigns(:all_words) + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(3)) + end + + def test_search_one_of_the_words + get :index, :id => 1, :q => 'recipe updating saving', :all_words => '' + assert_equal false, assigns(:all_words) + results = assigns(:results) + assert_not_nil results + assert_equal 3, results.size + assert results.include?(Issue.find(3)) + end + + def test_search_titles_only_without_result + get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1' + results = assigns(:results) + assert_not_nil results + assert_equal 0, results.size + end + + def test_search_titles_only + get :index, :id => 1, :q => 'recipe', :titles_only => '1' + assert_equal true, assigns(:titles_only) + results = assigns(:results) + assert_not_nil results + assert_equal 2, results.size + end + + def test_search_content + Issue.where(:id => 1).update_all("description = 'This is a searchkeywordinthecontent'") + get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => '' + assert_equal false, assigns(:titles_only) + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + end + + def test_search_with_offset + get :index, :q => 'coo', :offset => '20080806073000' + assert_response :success + results = assigns(:results) + assert results.any? + assert results.map(&:event_datetime).max < '20080806T073000'.to_time + end + + def test_search_previous_with_offset + get :index, :q => 'coo', :offset => '20080806073000', :previous => '1' + assert_response :success + results = assigns(:results) + assert results.any? + assert results.map(&:event_datetime).min >= '20080806T073000'.to_time + end + + def test_search_with_invalid_project_id + get :index, :id => 195, :q => 'recipe' + assert_response 404 + assert_nil assigns(:results) + end + + def test_quick_jump_to_issue + # issue of a public project + get :index, :q => "3" + assert_redirected_to '/issues/3' + + # issue of a private project + get :index, :q => "4" + assert_response :success + assert_template 'index' + end + + def test_large_integer + get :index, :q => '4615713488' + assert_response :success + assert_template 'index' + end + + def test_tokens_with_quotes + get :index, :id => 1, :q => '"good bye" hello "bye bye"' + assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens) + end + + def test_results_should_be_escaped_once + assert Issue.find(1).update_attributes(:subject => ' escaped_once', :description => ' escaped_once') + get :index, :q => 'escaped_once' + assert_response :success + assert_select '#search-results' do + assert_select 'dt.issue a', :text => /<subject>/ + assert_select 'dd', :text => /<description>/ + end + end + + def test_keywords_should_be_highlighted + assert Issue.find(1).update_attributes(:subject => 'subject highlighted', :description => 'description highlighted') + get :index, :q => 'highlighted' + assert_response :success + assert_select '#search-results' do + assert_select 'dt.issue a span.highlight', :text => 'highlighted' + assert_select 'dd span.highlight', :text => 'highlighted' + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/2647128cbfe83c34c563d9a0e3659f5a24700cec.svn-base --- a/.svn/pristine/26/2647128cbfe83c34c563d9a0e3659f5a24700cec.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2012 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 IssueStatusesHelper -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/265eb57f887ab5a88cc40ca110ab75d841eee5fd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/265eb57f887ab5a88cc40ca110ab75d841eee5fd.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,9 @@ +class AddCustomFieldsFormatStore < ActiveRecord::Migration + def up + add_column :custom_fields, :format_store, :text + end + + def down + remove_column :custom_fields, :format_store + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/266dfa6887b7544baedd932a1d45fffff0382a86.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/266dfa6887b7544baedd932a1d45fffff0382a86.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,388 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomField < ActiveRecord::Base + include Redmine::SubclassFactory + + has_many :custom_values, :dependent => :delete_all + has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id" + acts_as_list :scope => 'type = \'#{self.class}\'' + serialize :possible_values + + validates_presence_of :name, :field_format + validates_uniqueness_of :name, :scope => :type + validates_length_of :name, :maximum => 30 + validates_inclusion_of :field_format, :in => Proc.new { Redmine::CustomFieldFormat.available_formats } + validate :validate_custom_field + + before_validation :set_searchable + after_save :handle_multiplicity_change + after_save do |field| + if field.visible_changed? && field.visible + field.roles.clear + end + end + + scope :sorted, lambda { order("#{table_name}.position ASC") } + scope :visible, lambda {|*args| + user = args.shift || User.current + if user.admin? + # nop + elsif user.memberships.any? + where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" + + " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" + + " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" + + " WHERE m.user_id = ?)", + true, user.id) + else + where(:visible => true) + end + } + + def visible_by?(project, user=User.current) + visible? || user.admin? + end + + def field_format=(arg) + # cannot change format of a saved custom field + super if new_record? + end + + def set_searchable + # make sure these fields are not searchable + self.searchable = false if %w(int float date bool).include?(field_format) + # make sure only these fields can have multiple values + self.multiple = false unless %w(list user version).include?(field_format) + true + end + + def validate_custom_field + if self.field_format == "list" + errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? + errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array + end + + if regexp.present? + begin + Regexp.new(regexp) + rescue + errors.add(:regexp, :invalid) + end + end + + if default_value.present? && !valid_field_value?(default_value) + errors.add(:default_value, :invalid) + end + end + + def possible_values_options(obj=nil) + case field_format + when 'user', 'version' + if obj.respond_to?(:project) && obj.project + case field_format + when 'user' + obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]} + when 'version' + obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]} + end + elsif obj.is_a?(Array) + obj.collect {|o| possible_values_options(o)}.reduce(:&) + else + [] + end + when 'bool' + [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']] + else + possible_values || [] + end + end + + def possible_values(obj=nil) + case field_format + when 'user', 'version' + possible_values_options(obj).collect(&:last) + when 'bool' + ['1', '0'] + else + values = super() + if values.is_a?(Array) + values.each do |value| + value.force_encoding('UTF-8') if value.respond_to?(:force_encoding) + end + values + else + [] + end + end + end + + # Makes possible_values accept a multiline string + def possible_values=(arg) + if arg.is_a?(Array) + super(arg.compact.collect(&:strip).select {|v| !v.blank?}) + else + self.possible_values = arg.to_s.split(/[\n\r]+/) + end + end + + def cast_value(value) + casted = nil + unless value.blank? + case field_format + when 'string', 'text', 'list' + casted = value + when 'date' + casted = begin; value.to_date; rescue; nil end + when 'bool' + casted = (value == '1' ? true : false) + when 'int' + casted = value.to_i + when 'float' + casted = value.to_f + when 'user', 'version' + casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i)) + end + end + casted + end + + def value_from_keyword(keyword, customized) + possible_values_options = possible_values_options(customized) + if possible_values_options.present? + keyword = keyword.to_s.downcase + if v = possible_values_options.detect {|text, id| text.downcase == keyword} + if v.is_a?(Array) + v.last + else + v + end + end + else + keyword + end + end + + # Returns a ORDER BY clause that can used to sort customized + # objects by their value of the custom field. + # Returns nil if the custom field can not be used for sorting. + def order_statement + return nil if multiple? + case field_format + when 'string', 'text', 'list', 'date', 'bool' + # COALESCE is here to make sure that blank and NULL values are sorted equally + "COALESCE(#{join_alias}.value, '')" + when 'int', 'float' + # Make the database cast values into numeric + # Postgresql will raise an error if a value can not be casted! + # CustomValue validations should ensure that it doesn't occur + "CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,3))" + when 'user', 'version' + value_class.fields_for_order_statement(value_join_alias) + else + nil + end + end + + # Returns a GROUP BY clause that can used to group by custom value + # Returns nil if the custom field can not be used for grouping. + def group_statement + return nil if multiple? + case field_format + when 'list', 'date', 'bool', 'int' + order_statement + when 'user', 'version' + "COALESCE(#{join_alias}.value, '')" + else + nil + end + end + + def join_for_order_statement + case field_format + when 'user', 'version' + "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + + " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + + " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + + " AND #{join_alias}.custom_field_id = #{id}" + + " AND (#{visibility_by_project_condition})" + + " AND #{join_alias}.value <> ''" + + " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + + " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + + " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + + " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + + " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" + + " ON CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,0)) = #{value_join_alias}.id" + when 'int', 'float' + "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + + " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + + " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + + " AND #{join_alias}.custom_field_id = #{id}" + + " AND (#{visibility_by_project_condition})" + + " AND #{join_alias}.value <> ''" + + " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + + " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + + " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + + " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + when 'string', 'text', 'list', 'date', 'bool' + "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + + " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + + " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + + " AND #{join_alias}.custom_field_id = #{id}" + + " AND (#{visibility_by_project_condition})" + + " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + + " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + + " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + + " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + else + nil + end + end + + def join_alias + "cf_#{id}" + end + + def value_join_alias + join_alias + "_" + field_format + end + + def visibility_by_project_condition(project_key=nil, user=User.current) + if visible? || user.admin? + "1=1" + elsif user.anonymous? + "1=0" + else + project_key ||= "#{self.class.customized_class.table_name}.project_id" + "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" + + " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" + + " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" + + " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})" + end + end + + def self.visibility_condition + if user.admin? + "1=1" + elsif user.anonymous? + "#{table_name}.visible" + else + "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" + + " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" + + " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" + + " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})" + end + end + + def <=>(field) + position <=> field.position + end + + # Returns the class that values represent + def value_class + case field_format + when 'user', 'version' + field_format.classify.constantize + else + nil + end + end + + def self.customized_class + self.name =~ /^(.+)CustomField$/ + $1.constantize rescue nil + end + + # to move in project_custom_field + def self.for_all + where(:is_for_all => true).order('position').all + end + + def type_name + nil + end + + # Returns the error messages for the given value + # or an empty array if value is a valid value for the custom field + def validate_field_value(value) + errs = [] + if value.is_a?(Array) + if !multiple? + errs << ::I18n.t('activerecord.errors.messages.invalid') + end + if is_required? && value.detect(&:present?).nil? + errs << ::I18n.t('activerecord.errors.messages.blank') + end + value.each {|v| errs += validate_field_value_format(v)} + else + if is_required? && value.blank? + errs << ::I18n.t('activerecord.errors.messages.blank') + end + errs += validate_field_value_format(value) + end + errs + end + + # Returns true if value is a valid value for the custom field + def valid_field_value?(value) + validate_field_value(value).empty? + end + + def format_in?(*args) + args.include?(field_format) + end + + protected + + # Returns the error message for the given value regarding its format + def validate_field_value_format(value) + errs = [] + unless value.to_s == '' + errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp) + errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length && min_length > 0 && value.length < min_length + errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length && max_length > 0 && value.length > max_length + + # Format specific validations + case field_format + when 'int' + errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/ + when 'float' + begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end + when 'date' + errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end + when 'list' + errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value) + end + end + errs + end + + # Removes multiple values for the custom field after setting the multiple attribute to false + # We kepp the value with the highest id for each customized object + def handle_multiplicity_change + if !new_record? && multiple_was && !multiple + ids = custom_values. + where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" + + " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" + + " AND cve.id > #{CustomValue.table_name}.id)"). + pluck(:id) + + if ids.any? + custom_values.where(:id => ids).delete_all + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/26a05abc1949808b7bdc82c5b02ef9bf35bb7c86.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/26a05abc1949808b7bdc82c5b02ef9bf35bb7c86.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,118 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 TimelogCustomFieldsVisibilityTest < ActionController::TestCase + tests TimelogController + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issue_statuses, + :trackers, + :projects_trackers, + :enabled_modules, + :enumerations, + :workflows + + def setup + field_attributes = {:field_format => 'string', :is_for_all => true, :is_filter => true, :trackers => Tracker.all} + @fields = [] + @fields << (@field1 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 1', :visible => true))) + @fields << (@field2 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 2', :visible => false, :role_ids => [1, 2]))) + @fields << (@field3 = IssueCustomField.create!(field_attributes.merge(:name => 'Field 3', :visible => false, :role_ids => [1, 3]))) + @issue = Issue.generate!( + :author_id => 1, + :project_id => 1, + :tracker_id => 1, + :custom_field_values => {@field1.id => 'Value0', @field2.id => 'Value1', @field3.id => 'Value2'} + ) + TimeEntry.generate!(:issue => @issue) + + @user_with_role_on_other_project = User.generate! + User.add_to_project(@user_with_role_on_other_project, Project.find(2), Role.find(3)) + + @users_to_test = { + User.find(1) => [@field1, @field2, @field3], + User.find(3) => [@field1, @field2], + @user_with_role_on_other_project => [@field1], # should see field1 only on Project 1 + User.generate! => [@field1], + User.anonymous => [@field1] + } + + Member.where(:project_id => 1).each do |member| + member.destroy unless @users_to_test.keys.include?(member.principal) + end + end + + def test_index_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :index, :project_id => 1, :issue_id => @issue.id, :c => (['hours'] + @fields.map{|f| "issue.cf_#{f.id}"}) + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_select 'td', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name}" + else + assert_select 'td', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name}" + end + end + end + end + + def test_index_as_csv_should_show_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + get :index, :project_id => 1, :issue_id => @issue.id, :c => (['hours'] + @fields.map{|f| "issue.cf_#{f.id}"}), :format => 'csv' + @fields.each_with_index do |field, i| + if fields.include?(field) + assert_include "Value#{i}", response.body, "User #{user.id} was not able to view #{field.name} in CSV" + else + assert_not_include "Value#{i}", response.body, "User #{user.id} was able to view #{field.name} in CSV" + end + end + end + end + + def test_index_with_partial_custom_field_visibility_should_show_visible_custom_fields_only + Issue.delete_all + TimeEntry.delete_all + p1 = Project.generate! + p2 = Project.generate! + user = User.generate! + User.add_to_project(user, p1, Role.where(:id => [1, 3]).all) + User.add_to_project(user, p2, Role.where(:id => 3).all) + TimeEntry.generate!( + :issue => Issue.generate!(:project => p1, :tracker_id => 1, + :custom_field_values => {@field2.id => 'ValueA'})) + TimeEntry.generate!( + :issue => Issue.generate!(:project => p2, :tracker_id => 1, + :custom_field_values => {@field2.id => 'ValueB'})) + TimeEntry.generate!( + :issue => Issue.generate!(:project => p1, :tracker_id => 1, + :custom_field_values => {@field2.id => 'ValueC'})) + @request.session[:user_id] = user.id + get :index, :c => ["hours", "issue.cf_#{@field2.id}"] + assert_select 'td', :text => 'ValueA' + assert_select 'td', :text => 'ValueB', :count => 0 + assert_select 'td', :text => 'ValueC' + + get :index, :set_filter => '1', "issue.cf_#{@field2.id}" => '*' + assert_equal %w(ValueA ValueC), assigns(:entries).map{|i| i.issue.custom_field_value(@field2)}.sort + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/26d1f0e306c560c0d8ab8e97cc50b4f5ea6a381f.svn-base --- a/.svn/pristine/26/26d1f0e306c560c0d8ab8e97cc50b4f5ea6a381f.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 UserCustomField < CustomField - def type_name - :label_user_plural - end -end - diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/26d549e31ef6d77ef22f8be4d975f3ff8dfc16bc.svn-base --- a/.svn/pristine/26/26d549e31ef6d77ef22f8be4d975f3ff8dfc16bc.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1085 +0,0 @@ -# Redmine catalan translation: -# by Joan Duran - -ca: - # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) - direction: ltr - 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: "%d-%m-%Y" - short: "%e de %b" - long: "%a, %e de %b de %Y" - - day_names: [Diumenge, Dilluns, Dimarts, Dimecres, Dijous, Divendres, Dissabte] - abbr_day_names: [dg, dl, dt, dc, dj, dv, ds] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, Gener, Febrer, Març, Abril, Maig, Juny, Juliol, Agost, Setembre, Octubre, Novembre, Desembre] - abbr_month_names: [~, Gen, Feb, Mar, Abr, Mai, Jun, Jul, Ago, Set, Oct, Nov, Des] - # Used in date_select and datime_select. - order: - - :year - - :month - - :day - - time: - formats: - default: "%d-%m-%Y %H:%M" - time: "%H:%M" - short: "%e de %b, %H:%M" - long: "%a, %e de %b de %Y, %H:%M" - am: "am" - pm: "pm" - - datetime: - distance_in_words: - half_a_minute: "mig minut" - less_than_x_seconds: - one: "menys d'un segon" - other: "menys de %{count} segons" - x_seconds: - one: "1 segons" - other: "%{count} segons" - less_than_x_minutes: - one: "menys d'un minut" - other: "menys de %{count} minuts" - x_minutes: - one: "1 minut" - other: "%{count} minuts" - about_x_hours: - one: "aproximadament 1 hora" - other: "aproximadament %{count} hores" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "1 dia" - other: "%{count} dies" - about_x_months: - one: "aproximadament 1 mes" - other: "aproximadament %{count} mesos" - x_months: - one: "1 mes" - other: "%{count} mesos" - about_x_years: - one: "aproximadament 1 any" - other: "aproximadament %{count} anys" - over_x_years: - one: "més d'un any" - other: "més de %{count} anys" - almost_x_years: - one: "almost 1 year" - other: "almost %{count} years" - - 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: "i" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "1 error prohibited this %{model} from being saved" - other: "%{count} errors prohibited this %{model} from being saved" - messages: - inclusion: "no està inclòs a la llista" - exclusion: "està reservat" - invalid: "no és vàlid" - confirmation: "la confirmació no coincideix" - accepted: "s'ha d'acceptar" - empty: "no pot estar buit" - blank: "no pot estar en blanc" - too_long: "és massa llarg" - too_short: "és massa curt" - wrong_length: "la longitud és incorrecta" - taken: "ja s'està utilitzant" - not_a_number: "no és un número" - not_a_date: "no és una data vàlida" - greater_than: "ha de ser més gran que %{count}" - greater_than_or_equal_to: "ha de ser més gran o igual a %{count}" - equal_to: "ha de ser igual a %{count}" - less_than: "ha de ser menys que %{count}" - less_than_or_equal_to: "ha de ser menys o igual a %{count}" - odd: "ha de ser senar" - even: "ha de ser parell" - greater_than_start_date: "ha de ser superior que la data inicial" - not_same_project: "no pertany al mateix projecte" - circular_dependency: "Aquesta relació crearia una dependència circular" - cant_link_an_issue_with_a_descendant: "Un assumpte no es pot enllaçar a una de les seves subtasques" - - actionview_instancetag_blank_option: Seleccioneu - - general_text_No: 'No' - general_text_Yes: 'Si' - general_text_no: 'no' - general_text_yes: 'si' - general_lang_name: 'Català' - general_csv_separator: ';' - general_csv_decimal_separator: ',' - general_csv_encoding: ISO-8859-15 - general_pdf_encoding: UTF-8 - general_first_day_of_week: '1' - - notice_account_updated: "El compte s'ha actualitzat correctament." - notice_account_invalid_creditentials: Usuari o contrasenya invàlid - notice_account_password_updated: "La contrasenya s'ha modificat correctament." - notice_account_wrong_password: Contrasenya incorrecta - notice_account_register_done: "El compte s'ha creat correctament. Per a activar el compte, feu clic en l'enllaç que us han enviat per correu electrònic." - notice_account_unknown_email: Usuari desconegut. - notice_can_t_change_password: "Aquest compte utilitza una font d'autenticació externa. No és possible canviar la contrasenya." - notice_account_lost_email_sent: "S'ha enviat un correu electrònic amb instruccions per a seleccionar una contrasenya nova." - notice_account_activated: "El compte s'ha activat. Ara podeu entrar." - notice_successful_create: "S'ha creat correctament." - notice_successful_update: "S'ha modificat correctament." - notice_successful_delete: "S'ha suprimit correctament." - notice_successful_connection: "S'ha connectat correctament." - notice_file_not_found: "La pàgina a la que intenteu accedir no existeix o s'ha suprimit." - notice_locking_conflict: Un altre usuari ha actualitzat les dades. - notice_not_authorized: No teniu permís per a accedir a aquesta pàgina. - notice_email_sent: "S'ha enviat un correu electrònic a %{value}" - notice_email_error: "S'ha produït un error en enviar el correu (%{value})" - notice_feeds_access_key_reseted: "S'ha reiniciat la clau d'accés del RSS." - notice_api_access_key_reseted: "S'ha reiniciat la clau d'accés a l'API." - notice_failed_to_save_issues: "No s'han pogut desar %{count} assumptes de %{total} seleccionats: %{ids}." - notice_failed_to_save_members: "No s'han pogut desar els membres: %{errors}." - notice_no_issue_selected: "No s'ha seleccionat cap assumpte. Activeu els assumptes que voleu editar." - notice_account_pending: "S'ha creat el compte i ara està pendent de l'aprovació de l'administrador." - notice_default_data_loaded: "S'ha carregat correctament la configuració predeterminada." - notice_unable_delete_version: "No s'ha pogut suprimir la versió." - notice_unable_delete_time_entry: "No s'ha pogut suprimir l'entrada del registre de temps." - notice_issue_done_ratios_updated: "S'ha actualitzat el tant per cent dels assumptes." - - error_can_t_load_default_data: "No s'ha pogut carregar la configuració predeterminada: %{value} " - error_scm_not_found: "No s'ha trobat l'entrada o la revisió en el dipòsit." - error_scm_command_failed: "S'ha produït un error en intentar accedir al dipòsit: %{value}" - error_scm_annotate: "L'entrada no existeix o no s'ha pogut anotar." - error_issue_not_found_in_project: "No s'ha trobat l'assumpte o no pertany a aquest projecte" - error_no_tracker_in_project: "Aquest projecte no té seguidor associat. Comproveu els paràmetres del projecte." - error_no_default_issue_status: "No s'ha definit cap estat d'assumpte predeterminat. Comproveu la configuració (aneu a «Administració -> Estats de l'assumpte»)." - error_can_not_delete_custom_field: "No s'ha pogut suprimir el camp personalitat" - error_can_not_delete_tracker: "Aquest seguidor conté assumptes i no es pot suprimir." - error_can_not_remove_role: "Aquest rol s'està utilitzant i no es pot suprimir." - error_can_not_reopen_issue_on_closed_version: "Un assumpte assignat a una versió tancada no es pot tornar a obrir" - error_can_not_archive_project: "Aquest projecte no es pot arxivar" - error_issue_done_ratios_not_updated: "No s'ha actualitza el tant per cent dels assumptes." - error_workflow_copy_source: "Seleccioneu un seguidor o rol font" - error_workflow_copy_target: "Seleccioneu seguidors i rols objectiu" - error_unable_delete_issue_status: "No s'ha pogut suprimir l'estat de l'assumpte" - error_unable_to_connect: "No s'ha pogut connectar (%{value})" - warning_attachments_not_saved: "No s'han pogut desar %{count} fitxers." - - mail_subject_lost_password: "Contrasenya de %{value}" - mail_body_lost_password: "Per a canviar la contrasenya, feu clic en l'enllaç següent:" - mail_subject_register: "Activació del compte de %{value}" - mail_body_register: "Per a activar el compte, feu clic en l'enllaç següent:" - mail_body_account_information_external: "Podeu utilitzar el compte «%{value}» per a entrar." - mail_body_account_information: Informació del compte - mail_subject_account_activation_request: "Sol·licitud d'activació del compte de %{value}" - mail_body_account_activation_request: "S'ha registrat un usuari nou (%{value}). El seu compte està pendent d'aprovació:" - mail_subject_reminder: "%{count} assumptes venceran els següents %{days} dies" - mail_body_reminder: "%{count} assumptes que teniu assignades venceran els següents %{days} dies:" - mail_subject_wiki_content_added: "S'ha afegit la pàgina wiki «%{id}»" - mail_body_wiki_content_added: "En %{author} ha afegit la pàgina wiki «%{id}»." - mail_subject_wiki_content_updated: "S'ha actualitzat la pàgina wiki «%{id}»" - mail_body_wiki_content_updated: "En %{author} ha actualitzat la pàgina wiki «%{id}»." - - gui_validation_error: 1 error - gui_validation_error_plural: "%{count} errors" - - field_name: Nom - field_description: Descripció - field_summary: Resum - field_is_required: Necessari - field_firstname: Nom - field_lastname: Cognom - field_mail: Correu electrònic - field_filename: Fitxer - field_filesize: Mida - field_downloads: Baixades - field_author: Autor - field_created_on: Creat - field_updated_on: Actualitzat - field_field_format: Format - field_is_for_all: Per a tots els projectes - field_possible_values: Valores possibles - field_regexp: Expressió regular - field_min_length: Longitud mínima - field_max_length: Longitud màxima - field_value: Valor - field_category: Categoria - field_title: Títol - field_project: Projecte - field_issue: Assumpte - field_status: Estat - field_notes: Notes - field_is_closed: Assumpte tancat - field_is_default: Estat predeterminat - field_tracker: Seguidor - field_subject: Tema - field_due_date: Data de venciment - field_assigned_to: Assignat a - field_priority: Prioritat - field_fixed_version: Versió objectiu - field_user: Usuari - field_principal: Principal - field_role: Rol - field_homepage: Pàgina web - field_is_public: Públic - field_parent: Subprojecte de - field_is_in_roadmap: Assumptes mostrats en la planificació - field_login: Entrada - field_mail_notification: Notificacions per correu electrònic - field_admin: Administrador - field_last_login_on: Última connexió - field_language: Idioma - field_effective_date: Data - field_password: Contrasenya - field_new_password: Contrasenya nova - field_password_confirmation: Confirmació - field_version: Versió - field_type: Tipus - field_host: Ordinador - field_port: Port - field_account: Compte - field_base_dn: Base DN - field_attr_login: "Atribut d'entrada" - field_attr_firstname: Atribut del nom - field_attr_lastname: Atribut del cognom - field_attr_mail: Atribut del correu electrònic - field_onthefly: "Creació de l'usuari «al vol»" - field_start_date: Inici - field_done_ratio: "% realitzat" - field_auth_source: "Mode d'autenticació" - field_hide_mail: "Oculta l'adreça de correu electrònic" - field_comments: Comentari - field_url: URL - field_start_page: Pàgina inicial - field_subproject: Subprojecte - field_hours: Hores - field_activity: Activitat - field_spent_on: Data - field_identifier: Identificador - field_is_filter: "S'ha utilitzat com a filtre" - field_issue_to: Assumpte relacionat - field_delay: Retard - field_assignable: Es poden assignar assumptes a aquest rol - field_redirect_existing_links: Redirigeix els enllaços existents - field_estimated_hours: Temps previst - field_column_names: Columnes - field_time_entries: "Registre de temps" - field_time_zone: Zona horària - field_searchable: Es pot cercar - field_default_value: Valor predeterminat - field_comments_sorting: Mostra els comentaris - field_parent_title: Pàgina pare - field_editable: Es pot editar - field_watcher: Vigilància - field_identity_url: URL OpenID - field_content: Contingut - field_group_by: "Agrupa els resultats per" - field_sharing: Compartició - field_parent_issue: "Tasca pare" - - setting_app_title: "Títol de l'aplicació" - setting_app_subtitle: "Subtítol de l'aplicació" - setting_welcome_text: Text de benvinguda - setting_default_language: Idioma predeterminat - setting_login_required: Es necessita autenticació - setting_self_registration: Registre automàtic - setting_attachment_max_size: Mida màxima dels adjunts - setting_issues_export_limit: "Límit d'exportació d'assumptes" - setting_mail_from: "Adreça de correu electrònic d'emissió" - setting_bcc_recipients: Vincula els destinataris de les còpies amb carbó (bcc) - setting_plain_text_mail: només text pla (no HTML) - setting_host_name: "Nom de l'ordinador" - setting_text_formatting: Format del text - setting_wiki_compression: "Comprimeix l'historial del wiki" - setting_feeds_limit: Límit de contingut del canal - setting_default_projects_public: Els projectes nous són públics per defecte - setting_autofetch_changesets: Omple automàticament les publicacions - setting_sys_api_enabled: Habilita el WS per a la gestió del dipòsit - setting_commit_ref_keywords: Paraules claus per a la referència - setting_commit_fix_keywords: Paraules claus per a la correcció - setting_autologin: Entrada automàtica - setting_date_format: Format de la data - setting_time_format: Format de hora - setting_cross_project_issue_relations: "Permet les relacions d'assumptes entre projectes" - setting_issue_list_default_columns: "Columnes mostrades per defecte en la llista d'assumptes" - setting_emails_footer: Peu dels correus electrònics - setting_protocol: Protocol - setting_per_page_options: Opcions dels objectes per pàgina - setting_user_format: "Format de com mostrar l'usuari" - setting_activity_days_default: "Dies a mostrar l'activitat del projecte" - setting_display_subprojects_issues: "Mostra els assumptes d'un subprojecte en el projecte pare per defecte" - setting_enabled_scm: "Habilita l'SCM" - setting_mail_handler_body_delimiters: "Trunca els correus electrònics després d'una d'aquestes línies" - setting_mail_handler_api_enabled: "Habilita el WS per correus electrònics d'entrada" - setting_mail_handler_api_key: Clau API - setting_sequential_project_identifiers: Genera identificadors de projecte seqüencials - setting_gravatar_enabled: "Utilitza les icones d'usuari Gravatar" - setting_gravatar_default: "Imatge Gravatar predeterminada" - setting_diff_max_lines_displayed: Número màxim de línies amb diferències mostrades - setting_file_max_size_displayed: Mida màxima dels fitxers de text mostrats en línia - setting_repository_log_display_limit: Número màxim de revisions que es mostren al registre de fitxers - setting_openid: "Permet entrar i registrar-se amb l'OpenID" - setting_password_min_length: "Longitud mínima de la contrasenya" - setting_new_project_user_role_id: "Aquest rol es dóna a un usuari no administrador per a crear projectes" - setting_default_projects_modules: "Mòduls activats per defecte en els projectes nous" - setting_issue_done_ratio: "Calcula tant per cent realitzat de l'assumpte amb" - setting_issue_done_ratio_issue_status: "Utilitza l'estat de l'assumpte" - setting_issue_done_ratio_issue_field: "Utilitza el camp de l'assumpte" - setting_start_of_week: "Inicia les setmanes en" - setting_rest_api_enabled: "Habilita el servei web REST" - setting_cache_formatted_text: Cache formatted text - - permission_add_project: "Crea projectes" - permission_add_subprojects: "Crea subprojectes" - permission_edit_project: Edita el projecte - permission_select_project_modules: Selecciona els mòduls del projecte - permission_manage_members: Gestiona els membres - permission_manage_project_activities: "Gestiona les activitats del projecte" - permission_manage_versions: Gestiona les versions - permission_manage_categories: Gestiona les categories dels assumptes - permission_view_issues: "Visualitza els assumptes" - permission_add_issues: Afegeix assumptes - permission_edit_issues: Edita els assumptes - permission_manage_issue_relations: Gestiona les relacions dels assumptes - permission_add_issue_notes: Afegeix notes - permission_edit_issue_notes: Edita les notes - permission_edit_own_issue_notes: Edita les notes pròpies - permission_move_issues: Mou els assumptes - permission_delete_issues: Suprimeix els assumptes - permission_manage_public_queries: Gestiona les consultes públiques - permission_save_queries: Desa les consultes - permission_view_gantt: Visualitza la gràfica de Gantt - permission_view_calendar: Visualitza el calendari - permission_view_issue_watchers: Visualitza la llista de vigilàncies - permission_add_issue_watchers: Afegeix vigilàncies - permission_delete_issue_watchers: Suprimeix els vigilants - permission_log_time: Registra el temps invertit - permission_view_time_entries: Visualitza el temps invertit - permission_edit_time_entries: Edita els registres de temps - permission_edit_own_time_entries: Edita els registres de temps propis - permission_manage_news: Gestiona les noticies - permission_comment_news: Comenta les noticies - permission_manage_documents: Gestiona els documents - permission_view_documents: Visualitza els documents - permission_manage_files: Gestiona els fitxers - permission_view_files: Visualitza els fitxers - permission_manage_wiki: Gestiona el wiki - permission_rename_wiki_pages: Canvia el nom de les pàgines wiki - permission_delete_wiki_pages: Suprimeix les pàgines wiki - permission_view_wiki_pages: Visualitza el wiki - permission_view_wiki_edits: "Visualitza l'historial del wiki" - permission_edit_wiki_pages: Edita les pàgines wiki - permission_delete_wiki_pages_attachments: Suprimeix adjunts - permission_protect_wiki_pages: Protegeix les pàgines wiki - permission_manage_repository: Gestiona el dipòsit - permission_browse_repository: Navega pel dipòsit - permission_view_changesets: Visualitza els canvis realitzats - permission_commit_access: Accés a les publicacions - permission_manage_boards: Gestiona els taulers - permission_view_messages: Visualitza els missatges - permission_add_messages: Envia missatges - permission_edit_messages: Edita els missatges - permission_edit_own_messages: Edita els missatges propis - permission_delete_messages: Suprimeix els missatges - permission_delete_own_messages: Suprimeix els missatges propis - permission_export_wiki_pages: "Exporta les pàgines wiki" - permission_manage_subtasks: "Gestiona subtasques" - - project_module_issue_tracking: "Seguidor d'assumptes" - project_module_time_tracking: Seguidor de temps - project_module_news: Noticies - project_module_documents: Documents - project_module_files: Fitxers - project_module_wiki: Wiki - project_module_repository: Dipòsit - project_module_boards: Taulers - project_module_calendar: Calendari - project_module_gantt: Gantt - - label_user: Usuari - label_user_plural: Usuaris - label_user_new: Usuari nou - label_user_anonymous: Anònim - label_project: Projecte - label_project_new: Projecte nou - label_project_plural: Projectes - label_x_projects: - zero: cap projecte - one: 1 projecte - other: "%{count} projectes" - label_project_all: Tots els projectes - label_project_latest: Els últims projectes - label_issue: Assumpte - label_issue_new: Assumpte nou - label_issue_plural: Assumptes - label_issue_view_all: Visualitza tots els assumptes - label_issues_by: "Assumptes per %{value}" - label_issue_added: Assumpte afegit - label_issue_updated: Assumpte actualitzat - label_document: Document - label_document_new: Document nou - label_document_plural: Documents - label_document_added: Document afegit - label_role: Rol - label_role_plural: Rols - label_role_new: Rol nou - label_role_and_permissions: Rols i permisos - label_member: Membre - label_member_new: Membre nou - label_member_plural: Membres - label_tracker: Seguidor - label_tracker_plural: Seguidors - label_tracker_new: Seguidor nou - label_workflow: Flux de treball - label_issue_status: "Estat de l'assumpte" - label_issue_status_plural: "Estats de l'assumpte" - label_issue_status_new: Estat nou - label_issue_category: "Categoria de l'assumpte" - label_issue_category_plural: "Categories de l'assumpte" - label_issue_category_new: Categoria nova - label_custom_field: Camp personalitzat - label_custom_field_plural: Camps personalitzats - label_custom_field_new: Camp personalitzat nou - label_enumerations: Enumeracions - label_enumeration_new: Valor nou - label_information: Informació - label_information_plural: Informació - label_please_login: Entreu - label_register: Registre - label_login_with_open_id_option: "o entra amb l'OpenID" - label_password_lost: Contrasenya perduda - label_home: Inici - label_my_page: La meva pàgina - label_my_account: El meu compte - label_my_projects: Els meus projectes - label_my_page_block: "Els meus blocs de pàgina" - label_administration: Administració - label_login: Entra - label_logout: Surt - label_help: Ajuda - label_reported_issues: Assumptes informats - label_assigned_to_me_issues: Assumptes assignats a mi - label_last_login: Última connexió - label_registered_on: Informat el - label_activity: Activitat - label_overall_activity: Activitat global - label_user_activity: "Activitat de %{value}" - label_new: Nou - label_logged_as: Heu entrat com a - label_environment: Entorn - label_authentication: Autenticació - label_auth_source: "Mode d'autenticació" - label_auth_source_new: "Mode d'autenticació nou" - label_auth_source_plural: "Modes d'autenticació" - label_subproject_plural: Subprojectes - label_subproject_new: "Subprojecte nou" - label_and_its_subprojects: "%{value} i els seus subprojectes" - label_min_max_length: Longitud mín - max - label_list: Llist - label_date: Data - label_integer: Enter - label_float: Flotant - label_boolean: Booleà - label_string: Text - label_text: Text llarg - label_attribute: Atribut - label_attribute_plural: Atributs - label_download: "%{count} baixada" - label_download_plural: "%{count} baixades" - label_no_data: Sense dades a mostrar - label_change_status: "Canvia l'estat" - label_history: Historial - label_attachment: Fitxer - label_attachment_new: Fitxer nou - label_attachment_delete: Suprimeix el fitxer - label_attachment_plural: Fitxers - label_file_added: Fitxer afegit - label_report: Informe - label_report_plural: Informes - label_news: Noticies - label_news_new: Afegeix noticies - label_news_plural: Noticies - label_news_latest: Últimes noticies - label_news_view_all: Visualitza totes les noticies - label_news_added: Noticies afegides - label_settings: Paràmetres - label_overview: Resum - label_version: Versió - label_version_new: Versió nova - label_version_plural: Versions - label_close_versions: "Tanca les versions completades" - label_confirmation: Confirmació - label_export_to: "També disponible a:" - label_read: Llegeix... - label_public_projects: Projectes públics - label_open_issues: obert - label_open_issues_plural: oberts - label_closed_issues: tancat - label_closed_issues_plural: tancats - label_x_open_issues_abbr_on_total: - zero: 0 oberts / %{total} - one: 1 obert / %{total} - other: "%{count} oberts / %{total}" - label_x_open_issues_abbr: - zero: 0 oberts - one: 1 obert - other: "%{count} oberts" - label_x_closed_issues_abbr: - zero: 0 tancats - one: 1 tancat - other: "%{count} tancats" - label_total: Total - label_permissions: Permisos - label_current_status: Estat actual - label_new_statuses_allowed: Nous estats autoritzats - label_all: tots - label_none: cap - label_nobody: ningú - label_next: Següent - label_previous: Anterior - label_used_by: Utilitzat per - label_details: Detalls - label_add_note: Afegeix una nota - label_per_page: Per pàgina - label_calendar: Calendari - label_months_from: mesos des de - label_gantt: Gantt - label_internal: Intern - label_last_changes: "últims %{count} canvis" - label_change_view_all: Visualitza tots els canvis - label_personalize_page: Personalitza aquesta pàgina - label_comment: Comentari - label_comment_plural: Comentaris - label_x_comments: - zero: sense comentaris - one: 1 comentari - other: "%{count} comentaris" - label_comment_add: Afegeix un comentari - label_comment_added: Comentari afegit - label_comment_delete: Suprimeix comentaris - label_query: Consulta personalitzada - label_query_plural: Consultes personalitzades - label_query_new: Consulta nova - label_filter_add: Afegeix un filtre - label_filter_plural: Filtres - label_equals: és - label_not_equals: no és - label_in_less_than: en menys de - label_in_more_than: en més de - label_greater_or_equal: ">=" - label_less_or_equal: <= - label_in: en - label_today: avui - label_all_time: tot el temps - label_yesterday: ahir - label_this_week: aquesta setmana - label_last_week: "l'última setmana" - label_last_n_days: "els últims %{count} dies" - label_this_month: aquest més - label_last_month: "l'últim més" - label_this_year: aquest any - label_date_range: Abast de les dates - label_less_than_ago: fa menys de - label_more_than_ago: fa més de - label_ago: fa - label_contains: conté - label_not_contains: no conté - label_day_plural: dies - label_repository: Dipòsit - label_repository_plural: Dipòsits - label_browse: Navega - label_modification: "%{count} canvi" - label_modification_plural: "%{count} canvis" - label_branch: Branca - label_tag: Etiqueta - label_revision: Revisió - label_revision_plural: Revisions - label_revision_id: "Revisió %{value}" - label_associated_revisions: Revisions associades - label_added: afegit - label_modified: modificat - label_copied: copiat - label_renamed: reanomenat - label_deleted: suprimit - label_latest_revision: Última revisió - label_latest_revision_plural: Últimes revisions - label_view_revisions: Visualitza les revisions - label_view_all_revisions: "Visualitza totes les revisions" - label_max_size: Mida màxima - label_sort_highest: Mou a la part superior - label_sort_higher: Mou cap amunt - label_sort_lower: Mou cap avall - label_sort_lowest: Mou a la part inferior - label_roadmap: Planificació - label_roadmap_due_in: "Venç en %{value}" - label_roadmap_overdue: "%{value} tard" - label_roadmap_no_issues: No hi ha assumptes per a aquesta versió - label_search: Cerca - label_result_plural: Resultats - label_all_words: Totes les paraules - label_wiki: Wiki - label_wiki_edit: Edició wiki - label_wiki_edit_plural: Edicions wiki - label_wiki_page: Pàgina wiki - label_wiki_page_plural: Pàgines wiki - label_index_by_title: Ãndex per títol - label_index_by_date: Ãndex per data - label_current_version: Versió actual - label_preview: Previsualització - label_feed_plural: Canals - label_changes_details: Detalls de tots els canvis - label_issue_tracking: "Seguiment d'assumptes" - label_spent_time: Temps invertit - label_overall_spent_time: "Temps total invertit" - label_f_hour: "%{value} hora" - label_f_hour_plural: "%{value} hores" - label_time_tracking: Temps de seguiment - label_change_plural: Canvis - label_statistics: Estadístiques - label_commits_per_month: Publicacions per mes - label_commits_per_author: Publicacions per autor - label_view_diff: Visualitza les diferències - label_diff_inline: en línia - label_diff_side_by_side: costat per costat - label_options: Opcions - label_copy_workflow_from: Copia el flux de treball des de - label_permissions_report: Informe de permisos - label_watched_issues: Assumptes vigilats - label_related_issues: Assumptes relacionats - label_applied_status: Estat aplicat - label_loading: "S'està carregant..." - label_relation_new: Relació nova - label_relation_delete: Suprimeix la relació - label_relates_to: relacionat amb - label_duplicates: duplicats - label_duplicated_by: duplicat per - label_blocks: bloqueja - label_blocked_by: bloquejats per - label_precedes: anterior a - label_follows: posterior a - label_end_to_start: final al començament - label_end_to_end: final al final - label_start_to_start: començament al començament - label_start_to_end: començament al final - label_stay_logged_in: "Manté l'entrada" - label_disabled: inhabilitat - label_show_completed_versions: Mostra les versions completes - label_me: jo mateix - label_board: Fòrum - label_board_new: Fòrum nou - label_board_plural: Fòrums - label_board_locked: Bloquejat - label_board_sticky: Sticky - label_topic_plural: Temes - label_message_plural: Missatges - label_message_last: Últim missatge - label_message_new: Missatge nou - label_message_posted: Missatge afegit - label_reply_plural: Respostes - label_send_information: "Envia la informació del compte a l'usuari" - label_year: Any - label_month: Mes - label_week: Setmana - label_date_from: Des de - label_date_to: A - label_language_based: "Basat en l'idioma de l'usuari" - label_sort_by: "Ordena per %{value}" - label_send_test_email: Envia un correu electrònic de prova - label_feeds_access_key: "Clau d'accés del RSS" - label_missing_feeds_access_key: "Falta una clau d'accés del RSS" - label_feeds_access_key_created_on: "Clau d'accés del RSS creada fa %{value}" - label_module_plural: Mòduls - label_added_time_by: "Afegit per %{author} fa %{age}" - label_updated_time_by: "Actualitzat per %{author} fa %{age}" - label_updated_time: "Actualitzat fa %{value}" - label_jump_to_a_project: Salta al projecte... - label_file_plural: Fitxers - label_changeset_plural: Conjunt de canvis - label_default_columns: Columnes predeterminades - label_no_change_option: (sense canvis) - label_bulk_edit_selected_issues: Edita en bloc els assumptes seleccionats - label_theme: Tema - label_default: Predeterminat - label_search_titles_only: Cerca només en els títols - label_user_mail_option_all: "Per qualsevol esdeveniment en tots els meus projectes" - label_user_mail_option_selected: "Per qualsevol esdeveniment en els projectes seleccionats..." - label_user_mail_no_self_notified: "No vull ser notificat pels canvis que faig jo mateix" - label_registration_activation_by_email: activació del compte per correu electrònic - label_registration_manual_activation: activació del compte manual - label_registration_automatic_activation: activació del compte automàtica - label_display_per_page: "Per pàgina: %{value}" - label_age: Edat - label_change_properties: Canvia les propietats - label_general: General - label_more: Més - label_scm: SCM - label_plugins: Connectors - label_ldap_authentication: Autenticació LDAP - label_downloads_abbr: Baixades - label_optional_description: Descripció opcional - label_add_another_file: Afegeix un altre fitxer - label_preferences: Preferències - label_chronological_order: En ordre cronològic - label_reverse_chronological_order: En ordre cronològic invers - label_planning: Planificació - label_incoming_emails: "Correu electrònics d'entrada" - label_generate_key: Genera una clau - label_issue_watchers: Vigilàncies - label_example: Exemple - label_display: Mostra - label_sort: Ordena - label_ascending: Ascendent - label_descending: Descendent - label_date_from_to: Des de %{start} a %{end} - label_wiki_content_added: "S'ha afegit la pàgina wiki" - label_wiki_content_updated: "S'ha actualitzat la pàgina wiki" - label_group: Grup - label_group_plural: Grups - label_group_new: Grup nou - label_time_entry_plural: Temps invertit - label_version_sharing_hierarchy: "Amb la jerarquia del projecte" - label_version_sharing_system: "Amb tots els projectes" - label_version_sharing_descendants: "Amb tots els subprojectes" - label_version_sharing_tree: "Amb l'arbre del projecte" - label_version_sharing_none: "Sense compartir" - label_update_issue_done_ratios: "Actualitza el tant per cent dels assumptes realitzats" - label_copy_source: Font - label_copy_target: Objectiu - label_copy_same_as_target: "El mateix que l'objectiu" - label_display_used_statuses_only: "Mostra només els estats que utilitza aquest seguidor" - label_api_access_key: "Clau d'accés a l'API" - label_missing_api_access_key: "Falta una clau d'accés de l'API" - label_api_access_key_created_on: "Clau d'accés de l'API creada fa %{value}" - label_profile: Perfil - label_subtask_plural: Subtasques - label_project_copy_notifications: "Envia notificacions de correu electrònic durant la còpia del projecte" - - button_login: Entra - button_submit: Tramet - button_save: Desa - button_check_all: Activa-ho tot - button_uncheck_all: Desactiva-ho tot - button_delete: Suprimeix - button_create: Crea - button_create_and_continue: Crea i continua - button_test: Test - button_edit: Edit - button_add: Afegeix - button_change: Canvia - button_apply: Aplica - button_clear: Neteja - button_lock: Bloca - button_unlock: Desbloca - button_download: Baixa - button_list: Llista - button_view: Visualitza - button_move: Mou - button_move_and_follow: "Mou i segueix" - button_back: Enrere - button_cancel: Cancel·la - button_activate: Activa - button_sort: Ordena - button_log_time: "Registre de temps" - button_rollback: Torna a aquesta versió - button_watch: Vigila - button_unwatch: No vigilis - button_reply: Resposta - button_archive: Arxiva - button_unarchive: Desarxiva - button_reset: Reinicia - button_rename: Reanomena - button_change_password: Canvia la contrasenya - button_copy: Copia - button_copy_and_follow: "Copia i segueix" - button_annotate: Anota - button_update: Actualitza - button_configure: Configura - button_quote: Cita - button_duplicate: Duplica - button_show: Mostra - - status_active: actiu - status_registered: informat - status_locked: bloquejat - - version_status_open: oberta - version_status_locked: bloquejada - version_status_closed: tancada - - field_active: Actiu - - text_select_mail_notifications: "Seleccioneu les accions per les quals s'hauria d'enviar una notificació per correu electrònic." - text_regexp_info: ex. ^[A-Z0-9]+$ - text_min_max_length_info: 0 significa sense restricció - text_project_destroy_confirmation: Segur que voleu suprimir aquest projecte i les dades relacionades? - text_subprojects_destroy_warning: "També seran suprimits els seus subprojectes: %{value}." - text_workflow_edit: Seleccioneu un rol i un seguidor per a editar el flux de treball - text_are_you_sure: Segur? - text_journal_changed: "%{label} ha canviat de %{old} a %{new}" - text_journal_set_to: "%{label} s'ha establert a %{value}" - text_journal_deleted: "%{label} s'ha suprimit (%{old})" - text_journal_added: "S'ha afegit %{label} %{value}" - text_tip_issue_begin_day: "tasca que s'inicia aquest dia" - text_tip_issue_end_day: tasca que finalitza aquest dia - text_tip_issue_begin_end_day: "tasca que s'inicia i finalitza aquest dia" - text_caracters_maximum: "%{count} caràcters com a màxim." - text_caracters_minimum: "Com a mínim ha de tenir %{count} caràcters." - text_length_between: "Longitud entre %{min} i %{max} caràcters." - text_tracker_no_workflow: "No s'ha definit cap flux de treball per a aquest seguidor" - text_unallowed_characters: Caràcters no permesos - text_comma_separated: Es permeten valors múltiples (separats per una coma). - text_line_separated: "Es permeten diversos valors (una línia per cada valor)." - text_issues_ref_in_commit_messages: Referència i soluciona els assumptes en els missatges publicats - text_issue_added: "L'assumpte %{id} ha sigut informat per %{author}." - text_issue_updated: "L'assumpte %{id} ha sigut actualitzat per %{author}." - text_wiki_destroy_confirmation: Segur que voleu suprimir aquest wiki i tots els seus continguts? - text_issue_category_destroy_question: "Alguns assumptes (%{count}) estan assignats a aquesta categoria. Què voleu fer?" - text_issue_category_destroy_assignments: Suprimeix les assignacions de la categoria - text_issue_category_reassign_to: Torna a assignar els assumptes a aquesta categoria - text_user_mail_option: "Per als projectes no seleccionats, només rebreu notificacions sobre les coses que vigileu o que hi esteu implicat (ex. assumptes que en sou l'autor o hi esteu assignat)." - text_no_configuration_data: "Encara no s'han configurat els rols, seguidors, estats de l'assumpte i flux de treball.\nÉs altament recomanable que carregueu la configuració predeterminada. Podreu modificar-la un cop carregada." - text_load_default_configuration: Carrega la configuració predeterminada - text_status_changed_by_changeset: "Aplicat en el conjunt de canvis %{value}." - text_issues_destroy_confirmation: "Segur que voleu suprimir els assumptes seleccionats?" - text_select_project_modules: "Seleccioneu els mòduls a habilitar per a aquest projecte:" - text_default_administrator_account_changed: "S'ha canviat el compte d'administrador predeterminat" - text_file_repository_writable: Es pot escriure en el dipòsit de fitxers - text_plugin_assets_writable: Es pot escriure als connectors actius - text_rmagick_available: RMagick disponible (opcional) - text_destroy_time_entries_question: "S'han informat %{hours} hores en els assumptes que aneu a suprimir. Què voleu fer?" - text_destroy_time_entries: Suprimeix les hores informades - text_assign_time_entries_to_project: Assigna les hores informades al projecte - text_reassign_time_entries: "Torna a assignar les hores informades a aquest assumpte:" - text_user_wrote: "%{value} va escriure:" - text_enumeration_destroy_question: "%{count} objectes estan assignats a aquest valor." - text_enumeration_category_reassign_to: "Torna a assignar-los a aquest valor:" - text_email_delivery_not_configured: "El lliurament per correu electrònic no està configurat i les notificacions estan inhabilitades.\nConfigureu el servidor SMTP a config/configuration.yml i reinicieu l'aplicació per habilitar-lo." - text_repository_usernames_mapping: "Seleccioneu l'assignació entre els usuaris del Redmine i cada nom d'usuari trobat al dipòsit.\nEls usuaris amb el mateix nom d'usuari o correu del Redmine i del dipòsit s'assignaran automàticament." - text_diff_truncated: "... Aquestes diferències s'han trucat perquè excedeixen la mida màxima que es pot mostrar." - text_custom_field_possible_values_info: "Una línia per a cada valor" - text_wiki_page_destroy_question: "Aquesta pàgina té %{descendants} pàgines fill i descendents. Què voleu fer?" - text_wiki_page_nullify_children: "Deixa les pàgines fill com a pàgines arrel" - text_wiki_page_destroy_children: "Suprimeix les pàgines fill i tots els seus descendents" - text_wiki_page_reassign_children: "Reasigna les pàgines fill a aquesta pàgina pare" - text_own_membership_delete_confirmation: "Esteu a punt de suprimir algun o tots els vostres permisos i potser no podreu editar més aquest projecte.\nSegur que voleu continuar?" - text_zoom_in: Redueix - text_zoom_out: Amplia - - default_role_manager: Gestor - default_role_developer: Desenvolupador - default_role_reporter: Informador - default_tracker_bug: Error - default_tracker_feature: Característica - default_tracker_support: Suport - default_issue_status_new: Nou - default_issue_status_in_progress: In Progress - default_issue_status_resolved: Resolt - default_issue_status_feedback: Comentaris - default_issue_status_closed: Tancat - default_issue_status_rejected: Rebutjat - default_doc_category_user: "Documentació d'usuari" - default_doc_category_tech: Documentació tècnica - default_priority_low: Baixa - default_priority_normal: Normal - default_priority_high: Alta - default_priority_urgent: Urgent - default_priority_immediate: Immediata - default_activity_design: Disseny - default_activity_development: Desenvolupament - - enumeration_issue_priorities: Prioritat dels assumptes - enumeration_doc_categories: Categories del document - enumeration_activities: Activitats (seguidor de temps) - enumeration_system_activity: Activitat del sistema - - button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" - field_text: Text field - label_user_mail_option_only_owner: Only for things I am the owner of - setting_default_notification_option: Default notification option - label_user_mail_option_only_my_events: Only for things I watch or I'm involved in - label_user_mail_option_only_assigned: Only for things I am assigned to - label_user_mail_option_none: No events - field_member_of_group: Assignee's group - field_assigned_to_role: Assignee's role - notice_not_authorized_archived_project: The project you're trying to access has been archived. - label_principal_search: "Search for user or group:" - label_user_search: "Search for user:" - field_visible: Visible - setting_emails_header: Emails header - setting_commit_logtime_activity_id: Activity for logged time - text_time_logged_by_changeset: Applied in changeset %{value}. - setting_commit_logtime_enabled: Enable time logging - notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) - setting_gantt_items_limit: Maximum number of items displayed on the gantt chart - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: 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: Codificació dels missatges publicats - 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 - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - 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 assumpte - one: 1 assumpte - other: "%{count} assumptes" - 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.
    Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account - text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: tots - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: "Amb tots els subprojectes" - label_cross_project_tree: "Amb l'arbre del projecte" - label_cross_project_hierarchy: "Amb la jerarquia del projecte" - label_cross_project_system: "Amb tots els projectes" - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/26f01879d014f7cfdce074411219f0a0e3852861.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/26f01879d014f7cfdce074411219f0a0e3852861.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'action_view/helpers/form_helper' + +class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder + include Redmine::I18n + + (field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for) + + %w(date_select)).each do |selector| + src = <<-END_SRC + def #{selector}(field, options = {}) + label_for_field(field, options) + super(field, options.except(:label)).html_safe + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def select(field, choices, options = {}, html_options = {}) + label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe + end + + def time_zone_select(field, priority_zones = nil, options = {}, html_options = {}) + label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe + end + + # Returns a label tag for the given field + def label_for_field(field, options = {}) + return ''.html_safe if options.delete(:no_label) + text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label] + text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) + text += @template.content_tag("span", " *", :class => "required") if options.delete(:required) + @template.content_tag("label", text.html_safe, + :class => (@object && @object.errors[field].present? ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/26/26fe49aa17f017d2365927802d0e7bbb474fa34c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/26fe49aa17f017d2365927802d0e7bbb474fa34c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1120 @@ +# Redmine catalan translation: +# by Joan Duran + +ca: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + 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: "%d-%m-%Y" + short: "%e de %b" + long: "%a, %e de %b de %Y" + + day_names: [Diumenge, Dilluns, Dimarts, Dimecres, Dijous, Divendres, Dissabte] + abbr_day_names: [dg, dl, dt, dc, dj, dv, ds] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Gener, Febrer, Març, Abril, Maig, Juny, Juliol, Agost, Setembre, Octubre, Novembre, Desembre] + abbr_month_names: [~, Gen, Feb, Mar, Abr, Mai, Jun, Jul, Ago, Set, Oct, Nov, Des] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%d-%m-%Y %H:%M" + time: "%H:%M" + short: "%e de %b, %H:%M" + long: "%a, %e de %b de %Y, %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "mig minut" + less_than_x_seconds: + one: "menys d'un segon" + other: "menys de %{count} segons" + x_seconds: + one: "1 segons" + other: "%{count} segons" + less_than_x_minutes: + one: "menys d'un minut" + other: "menys de %{count} minuts" + x_minutes: + one: "1 minut" + other: "%{count} minuts" + about_x_hours: + one: "aproximadament 1 hora" + other: "aproximadament %{count} hores" + x_hours: + one: "1 hora" + other: "%{count} hores" + x_days: + one: "1 dia" + other: "%{count} dies" + about_x_months: + one: "aproximadament 1 mes" + other: "aproximadament %{count} mesos" + x_months: + one: "1 mes" + other: "%{count} mesos" + about_x_years: + one: "aproximadament 1 any" + other: "aproximadament %{count} anys" + over_x_years: + one: "més d'un any" + other: "més de %{count} anys" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + 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: "i" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "no està inclòs a la llista" + exclusion: "està reservat" + invalid: "no és vàlid" + confirmation: "la confirmació no coincideix" + accepted: "s'ha d'acceptar" + empty: "no pot estar buit" + blank: "no pot estar en blanc" + too_long: "és massa llarg" + too_short: "és massa curt" + wrong_length: "la longitud és incorrecta" + taken: "ja s'està utilitzant" + not_a_number: "no és un número" + not_a_date: "no és una data vàlida" + greater_than: "ha de ser més gran que %{count}" + greater_than_or_equal_to: "ha de ser més gran o igual a %{count}" + equal_to: "ha de ser igual a %{count}" + less_than: "ha de ser menys que %{count}" + less_than_or_equal_to: "ha de ser menys o igual a %{count}" + odd: "ha de ser senar" + even: "ha de ser parell" + greater_than_start_date: "ha de ser superior que la data inicial" + not_same_project: "no pertany al mateix projecte" + circular_dependency: "Aquesta relació crearia una dependència circular" + cant_link_an_issue_with_a_descendant: "Un assumpte no es pot enllaçar a una de les seves subtasques" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: Seleccioneu + + general_text_No: 'No' + general_text_Yes: 'Si' + general_text_no: 'no' + general_text_yes: 'si' + general_lang_name: 'Català' + general_csv_separator: ';' + general_csv_decimal_separator: ',' + general_csv_encoding: ISO-8859-15 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: "El compte s'ha actualitzat correctament." + notice_account_invalid_creditentials: Usuari o contrasenya invàlid + notice_account_password_updated: "La contrasenya s'ha modificat correctament." + notice_account_wrong_password: Contrasenya incorrecta + notice_account_register_done: "El compte s'ha creat correctament. Per a activar el compte, feu clic en l'enllaç que us han enviat per correu electrònic." + notice_account_unknown_email: Usuari desconegut. + notice_can_t_change_password: "Aquest compte utilitza una font d'autenticació externa. No és possible canviar la contrasenya." + notice_account_lost_email_sent: "S'ha enviat un correu electrònic amb instruccions per a seleccionar una contrasenya nova." + notice_account_activated: "El compte s'ha activat. Ara podeu entrar." + notice_successful_create: "S'ha creat correctament." + notice_successful_update: "S'ha modificat correctament." + notice_successful_delete: "S'ha suprimit correctament." + notice_successful_connection: "S'ha connectat correctament." + notice_file_not_found: "La pàgina a la que intenteu accedir no existeix o s'ha suprimit." + notice_locking_conflict: Un altre usuari ha actualitzat les dades. + notice_not_authorized: No teniu permís per a accedir a aquesta pàgina. + notice_email_sent: "S'ha enviat un correu electrònic a %{value}" + notice_email_error: "S'ha produït un error en enviar el correu (%{value})" + notice_feeds_access_key_reseted: "S'ha reiniciat la clau d'accés del Atom." + notice_api_access_key_reseted: "S'ha reiniciat la clau d'accés a l'API." + notice_failed_to_save_issues: "No s'han pogut desar %{count} assumptes de %{total} seleccionats: %{ids}." + notice_failed_to_save_members: "No s'han pogut desar els membres: %{errors}." + notice_no_issue_selected: "No s'ha seleccionat cap assumpte. Activeu els assumptes que voleu editar." + notice_account_pending: "S'ha creat el compte i ara està pendent de l'aprovació de l'administrador." + notice_default_data_loaded: "S'ha carregat correctament la configuració predeterminada." + notice_unable_delete_version: "No s'ha pogut suprimir la versió." + notice_unable_delete_time_entry: "No s'ha pogut suprimir l'entrada del registre de temps." + notice_issue_done_ratios_updated: "S'ha actualitzat el tant per cent dels assumptes." + + error_can_t_load_default_data: "No s'ha pogut carregar la configuració predeterminada: %{value} " + error_scm_not_found: "No s'ha trobat l'entrada o la revisió en el dipòsit." + error_scm_command_failed: "S'ha produït un error en intentar accedir al dipòsit: %{value}" + error_scm_annotate: "L'entrada no existeix o no s'ha pogut anotar." + error_issue_not_found_in_project: "No s'ha trobat l'assumpte o no pertany a aquest projecte" + error_no_tracker_in_project: "Aquest projecte no té seguidor associat. Comproveu els paràmetres del projecte." + error_no_default_issue_status: "No s'ha definit cap estat d'assumpte predeterminat. Comproveu la configuració (aneu a «Administració -> Estats de l'assumpte»)." + error_can_not_delete_custom_field: "No s'ha pogut suprimir el camp personalitat" + error_can_not_delete_tracker: "Aquest seguidor conté assumptes i no es pot suprimir." + error_can_not_remove_role: "Aquest rol s'està utilitzant i no es pot suprimir." + error_can_not_reopen_issue_on_closed_version: "Un assumpte assignat a una versió tancada no es pot tornar a obrir" + error_can_not_archive_project: "Aquest projecte no es pot arxivar" + error_issue_done_ratios_not_updated: "No s'ha actualitza el tant per cent dels assumptes." + error_workflow_copy_source: "Seleccioneu un seguidor o rol font" + error_workflow_copy_target: "Seleccioneu seguidors i rols objectiu" + error_unable_delete_issue_status: "No s'ha pogut suprimir l'estat de l'assumpte" + error_unable_to_connect: "No s'ha pogut connectar (%{value})" + warning_attachments_not_saved: "No s'han pogut desar %{count} fitxers." + + mail_subject_lost_password: "Contrasenya de %{value}" + mail_body_lost_password: "Per a canviar la contrasenya, feu clic en l'enllaç següent:" + mail_subject_register: "Activació del compte de %{value}" + mail_body_register: "Per a activar el compte, feu clic en l'enllaç següent:" + mail_body_account_information_external: "Podeu utilitzar el compte «%{value}» per a entrar." + mail_body_account_information: Informació del compte + mail_subject_account_activation_request: "Sol·licitud d'activació del compte de %{value}" + mail_body_account_activation_request: "S'ha registrat un usuari nou (%{value}). El seu compte està pendent d'aprovació:" + mail_subject_reminder: "%{count} assumptes venceran els següents %{days} dies" + mail_body_reminder: "%{count} assumptes que teniu assignades venceran els següents %{days} dies:" + mail_subject_wiki_content_added: "S'ha afegit la pàgina wiki «%{id}»" + mail_body_wiki_content_added: "En %{author} ha afegit la pàgina wiki «%{id}»." + mail_subject_wiki_content_updated: "S'ha actualitzat la pàgina wiki «%{id}»" + mail_body_wiki_content_updated: "En %{author} ha actualitzat la pàgina wiki «%{id}»." + + + field_name: Nom + field_description: Descripció + field_summary: Resum + field_is_required: Necessari + field_firstname: Nom + field_lastname: Cognom + field_mail: Correu electrònic + field_filename: Fitxer + field_filesize: Mida + field_downloads: Baixades + field_author: Autor + field_created_on: Creat + field_updated_on: Actualitzat + field_field_format: Format + field_is_for_all: Per a tots els projectes + field_possible_values: Valores possibles + field_regexp: Expressió regular + field_min_length: Longitud mínima + field_max_length: Longitud màxima + field_value: Valor + field_category: Categoria + field_title: Títol + field_project: Projecte + field_issue: Assumpte + field_status: Estat + field_notes: Notes + field_is_closed: Assumpte tancat + field_is_default: Estat predeterminat + field_tracker: Seguidor + field_subject: Tema + field_due_date: Data de venciment + field_assigned_to: Assignat a + field_priority: Prioritat + field_fixed_version: Versió objectiu + field_user: Usuari + field_principal: Principal + field_role: Rol + field_homepage: Pàgina web + field_is_public: Públic + field_parent: Subprojecte de + field_is_in_roadmap: Assumptes mostrats en la planificació + field_login: Entrada + field_mail_notification: Notificacions per correu electrònic + field_admin: Administrador + field_last_login_on: Última connexió + field_language: Idioma + field_effective_date: Data + field_password: Contrasenya + field_new_password: Contrasenya nova + field_password_confirmation: Confirmació + field_version: Versió + field_type: Tipus + field_host: Ordinador + field_port: Port + field_account: Compte + field_base_dn: Base DN + field_attr_login: "Atribut d'entrada" + field_attr_firstname: Atribut del nom + field_attr_lastname: Atribut del cognom + field_attr_mail: Atribut del correu electrònic + field_onthefly: "Creació de l'usuari «al vol»" + field_start_date: Inici + field_done_ratio: "% realitzat" + field_auth_source: "Mode d'autenticació" + field_hide_mail: "Oculta l'adreça de correu electrònic" + field_comments: Comentari + field_url: URL + field_start_page: Pàgina inicial + field_subproject: Subprojecte + field_hours: Hores + field_activity: Activitat + field_spent_on: Data + field_identifier: Identificador + field_is_filter: "S'ha utilitzat com a filtre" + field_issue_to: Assumpte relacionat + field_delay: Retard + field_assignable: Es poden assignar assumptes a aquest rol + field_redirect_existing_links: Redirigeix els enllaços existents + field_estimated_hours: Temps previst + field_column_names: Columnes + field_time_entries: "Registre de temps" + field_time_zone: Zona horària + field_searchable: Es pot cercar + field_default_value: Valor predeterminat + field_comments_sorting: Mostra els comentaris + field_parent_title: Pàgina pare + field_editable: Es pot editar + field_watcher: Vigilància + field_identity_url: URL OpenID + field_content: Contingut + field_group_by: "Agrupa els resultats per" + field_sharing: Compartició + field_parent_issue: "Tasca pare" + + setting_app_title: "Títol de l'aplicació" + setting_app_subtitle: "Subtítol de l'aplicació" + setting_welcome_text: Text de benvinguda + setting_default_language: Idioma predeterminat + setting_login_required: Es necessita autenticació + setting_self_registration: Registre automàtic + setting_attachment_max_size: Mida màxima dels adjunts + setting_issues_export_limit: "Límit d'exportació d'assumptes" + setting_mail_from: "Adreça de correu electrònic d'emissió" + setting_bcc_recipients: Vincula els destinataris de les còpies amb carbó (bcc) + setting_plain_text_mail: només text pla (no HTML) + setting_host_name: "Nom de l'ordinador" + setting_text_formatting: Format del text + setting_wiki_compression: "Comprimeix l'historial del wiki" + setting_feeds_limit: Límit de contingut del canal + setting_default_projects_public: Els projectes nous són públics per defecte + setting_autofetch_changesets: Omple automàticament les publicacions + setting_sys_api_enabled: Habilita el WS per a la gestió del dipòsit + setting_commit_ref_keywords: Paraules claus per a la referència + setting_commit_fix_keywords: Paraules claus per a la correcció + setting_autologin: Entrada automàtica + setting_date_format: Format de la data + setting_time_format: Format de hora + setting_cross_project_issue_relations: "Permet les relacions d'assumptes entre projectes" + setting_issue_list_default_columns: "Columnes mostrades per defecte en la llista d'assumptes" + setting_emails_footer: Peu dels correus electrònics + setting_protocol: Protocol + setting_per_page_options: Opcions dels objectes per pàgina + setting_user_format: "Format de com mostrar l'usuari" + setting_activity_days_default: "Dies a mostrar l'activitat del projecte" + setting_display_subprojects_issues: "Mostra els assumptes d'un subprojecte en el projecte pare per defecte" + setting_enabled_scm: "Habilita l'SCM" + setting_mail_handler_body_delimiters: "Trunca els correus electrònics després d'una d'aquestes línies" + setting_mail_handler_api_enabled: "Habilita el WS per correus electrònics d'entrada" + setting_mail_handler_api_key: Clau API + setting_sequential_project_identifiers: Genera identificadors de projecte seqüencials + setting_gravatar_enabled: "Utilitza les icones d'usuari Gravatar" + setting_gravatar_default: "Imatge Gravatar predeterminada" + setting_diff_max_lines_displayed: Número màxim de línies amb diferències mostrades + setting_file_max_size_displayed: Mida màxima dels fitxers de text mostrats en línia + setting_repository_log_display_limit: Número màxim de revisions que es mostren al registre de fitxers + setting_openid: "Permet entrar i registrar-se amb l'OpenID" + setting_password_min_length: "Longitud mínima de la contrasenya" + setting_new_project_user_role_id: "Aquest rol es dóna a un usuari no administrador per a crear projectes" + setting_default_projects_modules: "Mòduls activats per defecte en els projectes nous" + setting_issue_done_ratio: "Calcula tant per cent realitzat de l'assumpte amb" + setting_issue_done_ratio_issue_status: "Utilitza l'estat de l'assumpte" + setting_issue_done_ratio_issue_field: "Utilitza el camp de l'assumpte" + setting_start_of_week: "Inicia les setmanes en" + setting_rest_api_enabled: "Habilita el servei web REST" + setting_cache_formatted_text: Cache formatted text + + permission_add_project: "Crea projectes" + permission_add_subprojects: "Crea subprojectes" + permission_edit_project: Edita el projecte + permission_select_project_modules: Selecciona els mòduls del projecte + permission_manage_members: Gestiona els membres + permission_manage_project_activities: "Gestiona les activitats del projecte" + permission_manage_versions: Gestiona les versions + permission_manage_categories: Gestiona les categories dels assumptes + permission_view_issues: "Visualitza els assumptes" + permission_add_issues: Afegeix assumptes + permission_edit_issues: Edita els assumptes + permission_manage_issue_relations: Gestiona les relacions dels assumptes + permission_add_issue_notes: Afegeix notes + permission_edit_issue_notes: Edita les notes + permission_edit_own_issue_notes: Edita les notes pròpies + permission_move_issues: Mou els assumptes + permission_delete_issues: Suprimeix els assumptes + permission_manage_public_queries: Gestiona les consultes públiques + permission_save_queries: Desa les consultes + permission_view_gantt: Visualitza la gràfica de Gantt + permission_view_calendar: Visualitza el calendari + permission_view_issue_watchers: Visualitza la llista de vigilàncies + permission_add_issue_watchers: Afegeix vigilàncies + permission_delete_issue_watchers: Suprimeix els vigilants + permission_log_time: Registra el temps invertit + permission_view_time_entries: Visualitza el temps invertit + permission_edit_time_entries: Edita els registres de temps + permission_edit_own_time_entries: Edita els registres de temps propis + permission_manage_news: Gestiona les noticies + permission_comment_news: Comenta les noticies + permission_view_documents: Visualitza els documents + permission_manage_files: Gestiona els fitxers + permission_view_files: Visualitza els fitxers + permission_manage_wiki: Gestiona el wiki + permission_rename_wiki_pages: Canvia el nom de les pàgines wiki + permission_delete_wiki_pages: Suprimeix les pàgines wiki + permission_view_wiki_pages: Visualitza el wiki + permission_view_wiki_edits: "Visualitza l'historial del wiki" + permission_edit_wiki_pages: Edita les pàgines wiki + permission_delete_wiki_pages_attachments: Suprimeix adjunts + permission_protect_wiki_pages: Protegeix les pàgines wiki + permission_manage_repository: Gestiona el dipòsit + permission_browse_repository: Navega pel dipòsit + permission_view_changesets: Visualitza els canvis realitzats + permission_commit_access: Accés a les publicacions + permission_manage_boards: Gestiona els taulers + permission_view_messages: Visualitza els missatges + permission_add_messages: Envia missatges + permission_edit_messages: Edita els missatges + permission_edit_own_messages: Edita els missatges propis + permission_delete_messages: Suprimeix els missatges + permission_delete_own_messages: Suprimeix els missatges propis + permission_export_wiki_pages: "Exporta les pàgines wiki" + permission_manage_subtasks: "Gestiona subtasques" + + project_module_issue_tracking: "Seguidor d'assumptes" + project_module_time_tracking: Seguidor de temps + project_module_news: Noticies + project_module_documents: Documents + project_module_files: Fitxers + project_module_wiki: Wiki + project_module_repository: Dipòsit + project_module_boards: Taulers + project_module_calendar: Calendari + project_module_gantt: Gantt + + label_user: Usuari + label_user_plural: Usuaris + label_user_new: Usuari nou + label_user_anonymous: Anònim + label_project: Projecte + label_project_new: Projecte nou + label_project_plural: Projectes + label_x_projects: + zero: cap projecte + one: 1 projecte + other: "%{count} projectes" + label_project_all: Tots els projectes + label_project_latest: Els últims projectes + label_issue: Assumpte + label_issue_new: Assumpte nou + label_issue_plural: Assumptes + label_issue_view_all: Visualitza tots els assumptes + label_issues_by: "Assumptes per %{value}" + label_issue_added: Assumpte afegit + label_issue_updated: Assumpte actualitzat + label_document: Document + label_document_new: Document nou + label_document_plural: Documents + label_document_added: Document afegit + label_role: Rol + label_role_plural: Rols + label_role_new: Rol nou + label_role_and_permissions: Rols i permisos + label_member: Membre + label_member_new: Membre nou + label_member_plural: Membres + label_tracker: Seguidor + label_tracker_plural: Seguidors + label_tracker_new: Seguidor nou + label_workflow: Flux de treball + label_issue_status: "Estat de l'assumpte" + label_issue_status_plural: "Estats de l'assumpte" + label_issue_status_new: Estat nou + label_issue_category: "Categoria de l'assumpte" + label_issue_category_plural: "Categories de l'assumpte" + label_issue_category_new: Categoria nova + label_custom_field: Camp personalitzat + label_custom_field_plural: Camps personalitzats + label_custom_field_new: Camp personalitzat nou + label_enumerations: Enumeracions + label_enumeration_new: Valor nou + label_information: Informació + label_information_plural: Informació + label_please_login: Entreu + label_register: Registre + label_login_with_open_id_option: "o entra amb l'OpenID" + label_password_lost: Contrasenya perduda + label_home: Inici + label_my_page: La meva pàgina + label_my_account: El meu compte + label_my_projects: Els meus projectes + label_my_page_block: "Els meus blocs de pàgina" + label_administration: Administració + label_login: Entra + label_logout: Surt + label_help: Ajuda + label_reported_issues: Assumptes informats + label_assigned_to_me_issues: Assumptes assignats a mi + label_last_login: Última connexió + label_registered_on: Informat el + label_activity: Activitat + label_overall_activity: Activitat global + label_user_activity: "Activitat de %{value}" + label_new: Nou + label_logged_as: Heu entrat com a + label_environment: Entorn + label_authentication: Autenticació + label_auth_source: "Mode d'autenticació" + label_auth_source_new: "Mode d'autenticació nou" + label_auth_source_plural: "Modes d'autenticació" + label_subproject_plural: Subprojectes + label_subproject_new: "Subprojecte nou" + label_and_its_subprojects: "%{value} i els seus subprojectes" + label_min_max_length: Longitud mín - max + label_list: Llist + label_date: Data + label_integer: Enter + label_float: Flotant + label_boolean: Booleà + label_string: Text + label_text: Text llarg + label_attribute: Atribut + label_attribute_plural: Atributs + label_no_data: Sense dades a mostrar + label_change_status: "Canvia l'estat" + label_history: Historial + label_attachment: Fitxer + label_attachment_new: Fitxer nou + label_attachment_delete: Suprimeix el fitxer + label_attachment_plural: Fitxers + label_file_added: Fitxer afegit + label_report: Informe + label_report_plural: Informes + label_news: Noticies + label_news_new: Afegeix noticies + label_news_plural: Noticies + label_news_latest: Últimes noticies + label_news_view_all: Visualitza totes les noticies + label_news_added: Noticies afegides + label_settings: Paràmetres + label_overview: Resum + label_version: Versió + label_version_new: Versió nova + label_version_plural: Versions + label_close_versions: "Tanca les versions completades" + label_confirmation: Confirmació + label_export_to: "També disponible a:" + label_read: Llegeix... + label_public_projects: Projectes públics + label_open_issues: obert + label_open_issues_plural: oberts + label_closed_issues: tancat + label_closed_issues_plural: tancats + label_x_open_issues_abbr_on_total: + zero: 0 oberts / %{total} + one: 1 obert / %{total} + other: "%{count} oberts / %{total}" + label_x_open_issues_abbr: + zero: 0 oberts + one: 1 obert + other: "%{count} oberts" + label_x_closed_issues_abbr: + zero: 0 tancats + one: 1 tancat + other: "%{count} tancats" + label_total: Total + label_permissions: Permisos + label_current_status: Estat actual + label_new_statuses_allowed: Nous estats autoritzats + label_all: tots + label_none: cap + label_nobody: ningú + label_next: Següent + label_previous: Anterior + label_used_by: Utilitzat per + label_details: Detalls + label_add_note: Afegeix una nota + label_per_page: Per pàgina + label_calendar: Calendari + label_months_from: mesos des de + label_gantt: Gantt + label_internal: Intern + label_last_changes: "últims %{count} canvis" + label_change_view_all: Visualitza tots els canvis + label_personalize_page: Personalitza aquesta pàgina + label_comment: Comentari + label_comment_plural: Comentaris + label_x_comments: + zero: sense comentaris + one: 1 comentari + other: "%{count} comentaris" + label_comment_add: Afegeix un comentari + label_comment_added: Comentari afegit + label_comment_delete: Suprimeix comentaris + label_query: Consulta personalitzada + label_query_plural: Consultes personalitzades + label_query_new: Consulta nova + label_filter_add: Afegeix un filtre + label_filter_plural: Filtres + label_equals: és + label_not_equals: no és + label_in_less_than: en menys de + label_in_more_than: en més de + label_greater_or_equal: ">=" + label_less_or_equal: <= + label_in: en + label_today: avui + label_all_time: tot el temps + label_yesterday: ahir + label_this_week: aquesta setmana + label_last_week: "l'última setmana" + label_last_n_days: "els últims %{count} dies" + label_this_month: aquest més + label_last_month: "l'últim més" + label_this_year: aquest any + label_date_range: Abast de les dates + label_less_than_ago: fa menys de + label_more_than_ago: fa més de + label_ago: fa + label_contains: conté + label_not_contains: no conté + label_day_plural: dies + label_repository: Dipòsit + label_repository_plural: Dipòsits + label_browse: Navega + label_branch: Branca + label_tag: Etiqueta + label_revision: Revisió + label_revision_plural: Revisions + label_revision_id: "Revisió %{value}" + label_associated_revisions: Revisions associades + label_added: afegit + label_modified: modificat + label_copied: copiat + label_renamed: reanomenat + label_deleted: suprimit + label_latest_revision: Última revisió + label_latest_revision_plural: Últimes revisions + label_view_revisions: Visualitza les revisions + label_view_all_revisions: "Visualitza totes les revisions" + label_max_size: Mida màxima + label_sort_highest: Mou a la part superior + label_sort_higher: Mou cap amunt + label_sort_lower: Mou cap avall + label_sort_lowest: Mou a la part inferior + label_roadmap: Planificació + label_roadmap_due_in: "Venç en %{value}" + label_roadmap_overdue: "%{value} tard" + label_roadmap_no_issues: No hi ha assumptes per a aquesta versió + label_search: Cerca + label_result_plural: Resultats + label_all_words: Totes les paraules + label_wiki: Wiki + label_wiki_edit: Edició wiki + label_wiki_edit_plural: Edicions wiki + label_wiki_page: Pàgina wiki + label_wiki_page_plural: Pàgines wiki + label_index_by_title: Ãndex per títol + label_index_by_date: Ãndex per data + label_current_version: Versió actual + label_preview: Previsualització + label_feed_plural: Canals + label_changes_details: Detalls de tots els canvis + label_issue_tracking: "Seguiment d'assumptes" + label_spent_time: Temps invertit + label_overall_spent_time: "Temps total invertit" + label_f_hour: "%{value} hora" + label_f_hour_plural: "%{value} hores" + label_time_tracking: Temps de seguiment + label_change_plural: Canvis + label_statistics: Estadístiques + label_commits_per_month: Publicacions per mes + label_commits_per_author: Publicacions per autor + label_view_diff: Visualitza les diferències + label_diff_inline: en línia + label_diff_side_by_side: costat per costat + label_options: Opcions + label_copy_workflow_from: Copia el flux de treball des de + label_permissions_report: Informe de permisos + label_watched_issues: Assumptes vigilats + label_related_issues: Assumptes relacionats + label_applied_status: Estat aplicat + label_loading: "S'està carregant..." + label_relation_new: Relació nova + label_relation_delete: Suprimeix la relació + label_relates_to: relacionat amb + label_duplicates: duplicats + label_duplicated_by: duplicat per + label_blocks: bloqueja + label_blocked_by: bloquejats per + label_precedes: anterior a + label_follows: posterior a + label_end_to_start: final al començament + label_end_to_end: final al final + label_start_to_start: començament al començament + label_start_to_end: començament al final + label_stay_logged_in: "Manté l'entrada" + label_disabled: inhabilitat + label_show_completed_versions: Mostra les versions completes + label_me: jo mateix + label_board: Fòrum + label_board_new: Fòrum nou + label_board_plural: Fòrums + label_board_locked: Bloquejat + label_board_sticky: Sticky + label_topic_plural: Temes + label_message_plural: Missatges + label_message_last: Últim missatge + label_message_new: Missatge nou + label_message_posted: Missatge afegit + label_reply_plural: Respostes + label_send_information: "Envia la informació del compte a l'usuari" + label_year: Any + label_month: Mes + label_week: Setmana + label_date_from: Des de + label_date_to: A + label_language_based: "Basat en l'idioma de l'usuari" + label_sort_by: "Ordena per %{value}" + label_send_test_email: Envia un correu electrònic de prova + label_feeds_access_key: "Clau d'accés del Atom" + label_missing_feeds_access_key: "Falta una clau d'accés del Atom" + label_feeds_access_key_created_on: "Clau d'accés del Atom creada fa %{value}" + label_module_plural: Mòduls + label_added_time_by: "Afegit per %{author} fa %{age}" + label_updated_time_by: "Actualitzat per %{author} fa %{age}" + label_updated_time: "Actualitzat fa %{value}" + label_jump_to_a_project: Salta al projecte... + label_file_plural: Fitxers + label_changeset_plural: Conjunt de canvis + label_default_columns: Columnes predeterminades + label_no_change_option: (sense canvis) + label_bulk_edit_selected_issues: Edita en bloc els assumptes seleccionats + label_theme: Tema + label_default: Predeterminat + label_search_titles_only: Cerca només en els títols + label_user_mail_option_all: "Per qualsevol esdeveniment en tots els meus projectes" + label_user_mail_option_selected: "Per qualsevol esdeveniment en els projectes seleccionats..." + label_user_mail_no_self_notified: "No vull ser notificat pels canvis que faig jo mateix" + label_registration_activation_by_email: activació del compte per correu electrònic + label_registration_manual_activation: activació del compte manual + label_registration_automatic_activation: activació del compte automàtica + label_display_per_page: "Per pàgina: %{value}" + label_age: Edat + label_change_properties: Canvia les propietats + label_general: General + label_more: Més + label_scm: SCM + label_plugins: Connectors + label_ldap_authentication: Autenticació LDAP + label_downloads_abbr: Baixades + label_optional_description: Descripció opcional + label_add_another_file: Afegeix un altre fitxer + label_preferences: Preferències + label_chronological_order: En ordre cronològic + label_reverse_chronological_order: En ordre cronològic invers + label_planning: Planificació + label_incoming_emails: "Correu electrònics d'entrada" + label_generate_key: Genera una clau + label_issue_watchers: Vigilàncies + label_example: Exemple + label_display: Mostra + label_sort: Ordena + label_ascending: Ascendent + label_descending: Descendent + label_date_from_to: Des de %{start} a %{end} + label_wiki_content_added: "S'ha afegit la pàgina wiki" + label_wiki_content_updated: "S'ha actualitzat la pàgina wiki" + label_group: Grup + label_group_plural: Grups + label_group_new: Grup nou + label_time_entry_plural: Temps invertit + label_version_sharing_hierarchy: "Amb la jerarquia del projecte" + label_version_sharing_system: "Amb tots els projectes" + label_version_sharing_descendants: "Amb tots els subprojectes" + label_version_sharing_tree: "Amb l'arbre del projecte" + label_version_sharing_none: "Sense compartir" + label_update_issue_done_ratios: "Actualitza el tant per cent dels assumptes realitzats" + label_copy_source: Font + label_copy_target: Objectiu + label_copy_same_as_target: "El mateix que l'objectiu" + label_display_used_statuses_only: "Mostra només els estats que utilitza aquest seguidor" + label_api_access_key: "Clau d'accés a l'API" + label_missing_api_access_key: "Falta una clau d'accés de l'API" + label_api_access_key_created_on: "Clau d'accés de l'API creada fa %{value}" + label_profile: Perfil + label_subtask_plural: Subtasques + label_project_copy_notifications: "Envia notificacions de correu electrònic durant la còpia del projecte" + + button_login: Entra + button_submit: Tramet + button_save: Desa + button_check_all: Activa-ho tot + button_uncheck_all: Desactiva-ho tot + button_delete: Suprimeix + button_create: Crea + button_create_and_continue: Crea i continua + button_test: Test + button_edit: Edit + button_add: Afegeix + button_change: Canvia + button_apply: Aplica + button_clear: Neteja + button_lock: Bloca + button_unlock: Desbloca + button_download: Baixa + button_list: Llista + button_view: Visualitza + button_move: Mou + button_move_and_follow: "Mou i segueix" + button_back: Enrere + button_cancel: Cancel·la + button_activate: Activa + button_sort: Ordena + button_log_time: "Registre de temps" + button_rollback: Torna a aquesta versió + button_watch: Vigila + button_unwatch: No vigilis + button_reply: Resposta + button_archive: Arxiva + button_unarchive: Desarxiva + button_reset: Reinicia + button_rename: Reanomena + button_change_password: Canvia la contrasenya + button_copy: Copia + button_copy_and_follow: "Copia i segueix" + button_annotate: Anota + button_update: Actualitza + button_configure: Configura + button_quote: Cita + button_duplicate: Duplica + button_show: Mostra + + status_active: actiu + status_registered: informat + status_locked: bloquejat + + version_status_open: oberta + version_status_locked: bloquejada + version_status_closed: tancada + + field_active: Actiu + + text_select_mail_notifications: "Seleccioneu les accions per les quals s'hauria d'enviar una notificació per correu electrònic." + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 significa sense restricció + text_project_destroy_confirmation: Segur que voleu suprimir aquest projecte i les dades relacionades? + text_subprojects_destroy_warning: "També seran suprimits els seus subprojectes: %{value}." + text_workflow_edit: Seleccioneu un rol i un seguidor per a editar el flux de treball + text_are_you_sure: Segur? + text_journal_changed: "%{label} ha canviat de %{old} a %{new}" + text_journal_set_to: "%{label} s'ha establert a %{value}" + text_journal_deleted: "%{label} s'ha suprimit (%{old})" + text_journal_added: "S'ha afegit %{label} %{value}" + text_tip_issue_begin_day: "tasca que s'inicia aquest dia" + text_tip_issue_end_day: tasca que finalitza aquest dia + text_tip_issue_begin_end_day: "tasca que s'inicia i finalitza aquest dia" + text_caracters_maximum: "%{count} caràcters com a màxim." + text_caracters_minimum: "Com a mínim ha de tenir %{count} caràcters." + text_length_between: "Longitud entre %{min} i %{max} caràcters." + text_tracker_no_workflow: "No s'ha definit cap flux de treball per a aquest seguidor" + text_unallowed_characters: Caràcters no permesos + text_comma_separated: Es permeten valors múltiples (separats per una coma). + text_line_separated: "Es permeten diversos valors (una línia per cada valor)." + text_issues_ref_in_commit_messages: Referència i soluciona els assumptes en els missatges publicats + text_issue_added: "L'assumpte %{id} ha sigut informat per %{author}." + text_issue_updated: "L'assumpte %{id} ha sigut actualitzat per %{author}." + text_wiki_destroy_confirmation: Segur que voleu suprimir aquest wiki i tots els seus continguts? + text_issue_category_destroy_question: "Alguns assumptes (%{count}) estan assignats a aquesta categoria. Què voleu fer?" + text_issue_category_destroy_assignments: Suprimeix les assignacions de la categoria + text_issue_category_reassign_to: Torna a assignar els assumptes a aquesta categoria + text_user_mail_option: "Per als projectes no seleccionats, només rebreu notificacions sobre les coses que vigileu o que hi esteu implicat (ex. assumptes que en sou l'autor o hi esteu assignat)." + text_no_configuration_data: "Encara no s'han configurat els rols, seguidors, estats de l'assumpte i flux de treball.\nÉs altament recomanable que carregueu la configuració predeterminada. Podreu modificar-la un cop carregada." + text_load_default_configuration: Carrega la configuració predeterminada + text_status_changed_by_changeset: "Aplicat en el conjunt de canvis %{value}." + text_issues_destroy_confirmation: "Segur que voleu suprimir els assumptes seleccionats?" + text_select_project_modules: "Seleccioneu els mòduls a habilitar per a aquest projecte:" + text_default_administrator_account_changed: "S'ha canviat el compte d'administrador predeterminat" + text_file_repository_writable: Es pot escriure en el dipòsit de fitxers + text_plugin_assets_writable: Es pot escriure als connectors actius + text_rmagick_available: RMagick disponible (opcional) + text_destroy_time_entries_question: "S'han informat %{hours} hores en els assumptes que aneu a suprimir. Què voleu fer?" + text_destroy_time_entries: Suprimeix les hores informades + text_assign_time_entries_to_project: Assigna les hores informades al projecte + text_reassign_time_entries: "Torna a assignar les hores informades a aquest assumpte:" + text_user_wrote: "%{value} va escriure:" + text_enumeration_destroy_question: "%{count} objectes estan assignats a aquest valor." + text_enumeration_category_reassign_to: "Torna a assignar-los a aquest valor:" + text_email_delivery_not_configured: "El lliurament per correu electrònic no està configurat i les notificacions estan inhabilitades.\nConfigureu el servidor SMTP a config/configuration.yml i reinicieu l'aplicació per habilitar-lo." + text_repository_usernames_mapping: "Seleccioneu l'assignació entre els usuaris del Redmine i cada nom d'usuari trobat al dipòsit.\nEls usuaris amb el mateix nom d'usuari o correu del Redmine i del dipòsit s'assignaran automàticament." + text_diff_truncated: "... Aquestes diferències s'han trucat perquè excedeixen la mida màxima que es pot mostrar." + text_custom_field_possible_values_info: "Una línia per a cada valor" + text_wiki_page_destroy_question: "Aquesta pàgina té %{descendants} pàgines fill i descendents. Què voleu fer?" + text_wiki_page_nullify_children: "Deixa les pàgines fill com a pàgines arrel" + text_wiki_page_destroy_children: "Suprimeix les pàgines fill i tots els seus descendents" + text_wiki_page_reassign_children: "Reasigna les pàgines fill a aquesta pàgina pare" + text_own_membership_delete_confirmation: "Esteu a punt de suprimir algun o tots els vostres permisos i potser no podreu editar més aquest projecte.\nSegur que voleu continuar?" + text_zoom_in: Redueix + text_zoom_out: Amplia + + default_role_manager: Gestor + default_role_developer: Desenvolupador + default_role_reporter: Informador + default_tracker_bug: Error + default_tracker_feature: Característica + default_tracker_support: Suport + default_issue_status_new: Nou + default_issue_status_in_progress: In Progress + default_issue_status_resolved: Resolt + default_issue_status_feedback: Comentaris + default_issue_status_closed: Tancat + default_issue_status_rejected: Rebutjat + default_doc_category_user: "Documentació d'usuari" + default_doc_category_tech: Documentació tècnica + default_priority_low: Baixa + default_priority_normal: Normal + default_priority_high: Alta + default_priority_urgent: Urgent + default_priority_immediate: Immediata + default_activity_design: Disseny + default_activity_development: Desenvolupament + + enumeration_issue_priorities: Prioritat dels assumptes + enumeration_doc_categories: Categories del document + enumeration_activities: Activitats (seguidor de temps) + enumeration_system_activity: Activitat del sistema + + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: 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: Codificació dels missatges publicats + 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 assumpte + one: 1 assumpte + other: "%{count} assumptes" + 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.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: tots + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: "Amb tots els subprojectes" + label_cross_project_tree: "Amb l'arbre del projecte" + label_cross_project_hierarchy: "Amb la jerarquia del projecte" + label_cross_project_system: "Amb tots els projectes" + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: Total + text_scm_config: You can configure your SCM commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: SCM command is not available. Please check settings on the administration panel. + setting_emails_header: Email header + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) + label_link: Link + label_only: only + label_drop_down_list: drop-down list + label_checkboxes: checkboxes + label_link_values_to: Link values to URL + setting_force_default_language_for_anonymous: Force default language for anonymous + users + setting_force_default_language_for_loggedin: Force default language for logged-in + users + label_custom_field_select_type: Select the type of object to which the custom field + is to be attached + label_check_for_updates: Check for updates + label_latest_compatible_version: Latest compatible version + label_unknown_plugin: Unknown plugin + label_radio_buttons: radio buttons diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/27/27ba65ff9d28d2f3bd5d9b790ec776816aaa1eb7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/27/27ba65ff9d28d2f3bd5d9b790ec776816aaa1eb7.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,609 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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/setup' + + class GitAdapterTest < ActiveSupport::TestCase + REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s + + FELIX_HEX = "Felix Sch\xC3\xA4fer" + CHAR_1_HEX = "\xc3\x9c" + + ## Git, Mercurial and CVS path encodings are binary. + ## Subversion supports URL encoding for path. + ## Redmine Mercurial adapter and extension use URL encoding. + ## Git accepts only binary path in command line parameter. + ## So, there is no way to use binary command line parameter in JRuby. + JRUBY_SKIP = (RUBY_PLATFORM == 'java') + JRUBY_SKIP_STR = "TODO: This test fails in JRuby" + + if File.directory?(REPOSITORY_PATH) + ## Ruby uses ANSI api to fork a process on Windows. + ## Japanese Shift_JIS and Traditional Chinese Big5 have 0x5c(backslash) problem + ## and these are incompatible with ASCII. + ## Git for Windows (msysGit) changed internal API from ANSI to Unicode in 1.7.10 + ## http://code.google.com/p/msysgit/issues/detail?id=80 + ## So, Latin-1 path tests fail on Japanese Windows + WINDOWS_PASS = (Redmine::Platform.mswin? && + Redmine::Scm::Adapters::GitAdapter.client_version_above?([1, 7, 10])) + WINDOWS_SKIP_STR = "TODO: This test fails in Git for Windows above 1.7.10" + + def setup + adapter_class = Redmine::Scm::Adapters::GitAdapter + assert adapter_class + assert adapter_class.client_command + assert_equal true, adapter_class.client_available + assert_equal true, adapter_class.client_version_above?([1]) + assert_equal true, adapter_class.client_version_above?([1, 0]) + + @adapter = Redmine::Scm::Adapters::GitAdapter.new( + REPOSITORY_PATH, + nil, + nil, + nil, + 'ISO-8859-1' + ) + assert @adapter + @char_1 = CHAR_1_HEX.dup + @str_felix_hex = FELIX_HEX.dup + if @char_1.respond_to?(:force_encoding) + @char_1.force_encoding('UTF-8') + @str_felix_hex.force_encoding('ASCII-8BIT') + end + end + + def test_scm_version + to_test = { "git version 1.7.3.4\n" => [1,7,3,4], + "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_branches + brs = [] + @adapter.branches.each do |b| + brs << b + end + assert_equal 6, brs.length + br_issue_8857 = brs[0] + assert_equal 'issue-8857', br_issue_8857.to_s + assert_equal '2a682156a3b6e77a8bf9cd4590e8db757f3c6c78', br_issue_8857.revision + assert_equal br_issue_8857.scmid, br_issue_8857.revision + assert_equal false, br_issue_8857.is_default + br_latin_1_path = brs[1] + assert_equal 'latin-1-path-encoding', br_latin_1_path.to_s + assert_equal '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127', br_latin_1_path.revision + assert_equal br_latin_1_path.scmid, br_latin_1_path.revision + assert_equal false, br_latin_1_path.is_default + br_master = brs[2] + assert_equal 'master', br_master.to_s + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', br_master.revision + assert_equal br_master.scmid, br_master.revision + assert_equal false, br_master.is_default + br_master_20120212 = brs[3] + assert_equal 'master-20120212', br_master_20120212.to_s + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', br_master_20120212.revision + assert_equal br_master_20120212.scmid, br_master_20120212.revision + assert_equal true, br_master_20120212.is_default + br_latin_1 = brs[-2] + assert_equal 'test-latin-1', br_latin_1.to_s + assert_equal '67e7792ce20ccae2e4bb73eed09bb397819c8834', br_latin_1.revision + assert_equal br_latin_1.scmid, br_latin_1.revision + assert_equal false, br_latin_1.is_default + br_test = brs[-1] + assert_equal 'test_branch', br_test.to_s + assert_equal 'fba357b886984ee71185ad2065e65fc0417d9b92', br_test.revision + assert_equal br_test.scmid, br_test.revision + assert_equal false, br_test.is_default + end + + def test_default_branch + assert_equal 'master-20120212', @adapter.default_branch + end + + def test_tags + assert_equal [ + "tag00.lightweight", + "tag01.annotated", + ], @adapter.tags + end + + def test_revisions_master_all + revs1 = [] + @adapter.revisions('', nil, "master",{}) do |rev| + revs1 << rev + end + assert_equal 15, revs1.length + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs1[ 0].identifier + assert_equal '7234cb2750b63f47bff735edc50a1c0a433c2518', revs1[-1].identifier + + revs2 = [] + @adapter.revisions('', nil, "master", + {:reverse => true}) do |rev| + revs2 << rev + end + assert_equal 15, revs2.length + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs2[-1].identifier + assert_equal '7234cb2750b63f47bff735edc50a1c0a433c2518', revs2[ 0].identifier + end + + def test_revisions_master_merged_rev + revs1 = [] + @adapter.revisions('', + "713f4944648826f558cf548222f813dabe7cbb04", + "master", + {:reverse => true}) do |rev| + revs1 << rev + end + assert_equal 8, revs1.length + assert_equal 'fba357b886984ee71185ad2065e65fc0417d9b92', revs1[ 0].identifier + assert_equal '7e61ac704deecde634b51e59daa8110435dcb3da', revs1[ 1].identifier + # 4a07fe31b is not a child of 713f49446 + assert_equal '4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8', revs1[ 2].identifier + # Merged revision + assert_equal '32ae898b720c2f7eec2723d5bdd558b4cb2d3ddf', revs1[ 3].identifier + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs1[-1].identifier + + revs2 = [] + @adapter.revisions('', + "fba357b886984ee71185ad2065e65fc0417d9b92", + "master", + {:reverse => true}) do |rev| + revs2 << rev + end + assert_equal 7, revs2.length + assert_equal '7e61ac704deecde634b51e59daa8110435dcb3da', revs2[ 0].identifier + # 4a07fe31b is not a child of fba357b8869 + assert_equal '4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8', revs2[ 1].identifier + # Merged revision + assert_equal '32ae898b720c2f7eec2723d5bdd558b4cb2d3ddf', revs2[ 2].identifier + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs2[-1].identifier + end + + def test_revisions_branch_latin_1_path_encoding_all + revs1 = [] + @adapter.revisions('', nil, "latin-1-path-encoding",{}) do |rev| + revs1 << rev + end + assert_equal 8, revs1.length + assert_equal '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127', revs1[ 0].identifier + assert_equal '7234cb2750b63f47bff735edc50a1c0a433c2518', revs1[-1].identifier + + revs2 = [] + @adapter.revisions('', nil, "latin-1-path-encoding", + {:reverse => true}) do |rev| + revs2 << rev + end + assert_equal 8, revs2.length + assert_equal '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127', revs2[-1].identifier + assert_equal '7234cb2750b63f47bff735edc50a1c0a433c2518', revs2[ 0].identifier + end + + def test_revisions_branch_latin_1_path_encoding_with_rev + revs1 = [] + @adapter.revisions('', + '7234cb2750b63f47bff735edc50a1c0a433c2518', + "latin-1-path-encoding", + {:reverse => true}) do |rev| + revs1 << rev + end + assert_equal 7, revs1.length + assert_equal '899a15dba03a3b350b89c3f537e4bbe02a03cdc9', revs1[ 0].identifier + assert_equal '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127', revs1[-1].identifier + + revs2 = [] + @adapter.revisions('', + '57ca437c0acbbcb749821fdf3726a1367056d364', + "latin-1-path-encoding", + {:reverse => true}) do |rev| + revs2 << rev + end + assert_equal 3, revs2.length + assert_equal '4fc55c43bf3d3dc2efb66145365ddc17639ce81e', revs2[ 0].identifier + assert_equal '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127', revs2[-1].identifier + end + + def test_revisions_invalid_rev + assert_equal [], @adapter.revisions('', '1234abcd', "master") + assert_raise Redmine::Scm::Adapters::CommandFailed do + revs1 = [] + @adapter.revisions('', + '1234abcd', + "master", + {:reverse => true}) do |rev| + revs1 << rev + end + end + end + + def test_revisions_includes_master_two_revs + revs1 = [] + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c'], + :excludes => ['4f26664364207fa8b1af9f8722647ab2d4ac5d43']}) do |rev| + revs1 << rev + end + assert_equal 2, revs1.length + assert_equal 'ed5bb786bbda2dee66a2d50faf51429dbc043a7b', revs1[ 0].identifier + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs1[-1].identifier + end + + def test_revisions_includes_master_two_revs_from_origin + revs1 = [] + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['899a15dba03a3b350b89c3f537e4bbe02a03cdc9'], + :excludes => []}) do |rev| + revs1 << rev + end + assert_equal 2, revs1.length + assert_equal '7234cb2750b63f47bff735edc50a1c0a433c2518', revs1[ 0].identifier + assert_equal '899a15dba03a3b350b89c3f537e4bbe02a03cdc9', revs1[ 1].identifier + end + + def test_revisions_includes_merged_revs + revs1 = [] + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c'], + :excludes => ['fba357b886984ee71185ad2065e65fc0417d9b92']}) do |rev| + revs1 << rev + end + assert_equal 7, revs1.length + assert_equal '7e61ac704deecde634b51e59daa8110435dcb3da', revs1[ 0].identifier + assert_equal '4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8', revs1[ 1].identifier + assert_equal '32ae898b720c2f7eec2723d5bdd558b4cb2d3ddf', revs1[ 2].identifier + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs1[-1].identifier + end + + def test_revisions_includes_two_heads + revs1 = [] + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', + '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127'], + :excludes => ['4f26664364207fa8b1af9f8722647ab2d4ac5d43', + '4fc55c43bf3d3dc2efb66145365ddc17639ce81e']}) do |rev| + revs1 << rev + end + assert_equal 4, revs1.length + assert_equal 'ed5bb786bbda2dee66a2d50faf51429dbc043a7b', revs1[ 0].identifier + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs1[ 1].identifier + assert_equal '64f1f3e89ad1cb57976ff0ad99a107012ba3481d', revs1[-2].identifier + assert_equal '1ca7f5ed374f3cb31a93ae5215c2e25cc6ec5127', revs1[-1].identifier + end + + def test_revisions_disjointed_histories_revisions + revs1 = [] + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', + '92397af84d22f27389c822848ecd5b463c181583'], + :excludes => ['95488a44bc25f7d1f97d775a31359539ff333a63', + '4f26664364207fa8b1af9f8722647ab2d4ac5d43'] }) do |rev| + revs1 << rev + end + assert_equal 4, revs1.length + assert_equal 'ed5bb786bbda2dee66a2d50faf51429dbc043a7b', revs1[ 0].identifier + assert_equal '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', revs1[ 1].identifier + assert_equal 'bc201c95999c4f10d018b0aa03b541cd6a2ff0ee', revs1[-2].identifier + assert_equal '92397af84d22f27389c822848ecd5b463c181583', revs1[-1].identifier + end + + def test_revisions_invalid_rev_excludes + assert_equal [], + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c'], + :excludes => ['0123abcd4567']}) + assert_raise Redmine::Scm::Adapters::CommandFailed do + revs1 = [] + @adapter.revisions('', nil, nil, + {:reverse => true, + :includes => ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c'], + :excludes => ['0123abcd4567']}) do |rev| + revs1 << rev + end + end + end + + def test_getting_revisions_with_spaces_in_filename + assert_equal 1, @adapter.revisions("filemane with spaces.txt", + nil, "master").length + end + + def test_parents + revs1 = [] + @adapter.revisions('', + nil, + "master", + {:reverse => true}) do |rev| + revs1 << rev + end + assert_equal 15, revs1.length + assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", + revs1[0].identifier + assert_equal nil, revs1[0].parents + assert_equal "899a15dba03a3b350b89c3f537e4bbe02a03cdc9", + revs1[1].identifier + assert_equal 1, revs1[1].parents.length + assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", + revs1[1].parents[0] + assert_equal "32ae898b720c2f7eec2723d5bdd558b4cb2d3ddf", + revs1[10].identifier + assert_equal 2, revs1[10].parents.length + assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", + revs1[10].parents[0] + assert_equal "7e61ac704deecde634b51e59daa8110435dcb3da", + revs1[10].parents[1] + end + + def test_getting_revisions_with_leading_and_trailing_spaces_in_filename + assert_equal " filename with a leading space.txt ", + @adapter.revisions(" filename with a leading space.txt ", + nil, "master")[0].paths[0][:path] + end + + def test_getting_entries_with_leading_and_trailing_spaces_in_filename + assert_equal " filename with a leading space.txt ", + @adapter.entries('', + '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c')[3].name + end + + def test_annotate + annotate = @adapter.annotate('sources/watchers_controller.rb') + assert_kind_of Redmine::Scm::Adapters::Annotate, annotate + assert_equal 41, annotate.lines.size + assert_equal "# This program is free software; you can redistribute it and/or", + annotate.lines[4].strip + assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", + annotate.revisions[4].identifier + assert_equal "jsmith", annotate.revisions[4].author + end + + def test_annotate_moved_file + annotate = @adapter.annotate('renamed_test.txt') + assert_kind_of Redmine::Scm::Adapters::Annotate, annotate + assert_equal 2, annotate.lines.size + end + + def test_last_rev + last_rev = @adapter.lastrev("README", + "4f26664364207fa8b1af9f8722647ab2d4ac5d43") + assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", last_rev.scmid + assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", last_rev.identifier + assert_equal "Adam Soltys ", last_rev.author + assert_equal "2009-06-24 05:27:38".to_time, last_rev.time + end + + def test_last_rev_with_spaces_in_filename + last_rev = @adapter.lastrev("filemane with spaces.txt", + "ed5bb786bbda2dee66a2d50faf51429dbc043a7b") + last_rev_author = last_rev.author + assert_equal "ed5bb786bbda2dee66a2d50faf51429dbc043a7b", last_rev.scmid + assert_equal "ed5bb786bbda2dee66a2d50faf51429dbc043a7b", last_rev.identifier + assert_equal "#{@str_felix_hex} ", + last_rev.author + assert_equal "2010-09-18 19:59:46".to_time, last_rev.time + end + + def test_latin_1_path + if WINDOWS_PASS + puts WINDOWS_SKIP_STR + elsif JRUBY_SKIP + puts JRUBY_SKIP_STR + else + p2 = "latin-1-dir/test-#{@char_1}-2.txt" + ['4fc55c43bf3d3dc2efb66145365ddc17639ce81e', '4fc55c43bf3'].each do |r1| + assert @adapter.diff(p2, r1) + assert @adapter.cat(p2, r1) + assert_equal 1, @adapter.annotate(p2, r1).lines.length + ['64f1f3e89ad1cb57976ff0ad99a107012ba3481d', '64f1f3e89ad1cb5797'].each do |r2| + assert @adapter.diff(p2, r1, r2) + end + end + end + end + + def test_latin_1_user_annotate + ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', '83ca5fd546063a'].each do |r1| + annotate = @adapter.annotate(" filename with a leading space.txt ", r1) + assert_kind_of Redmine::Scm::Adapters::Annotate, annotate + assert_equal 1, annotate.lines.size + assert_equal "And this is a file with a leading and trailing space...", + annotate.lines[0].strip + assert_equal "83ca5fd546063a3c7dc2e568ba3355661a9e2b2c", + annotate.revisions[0].identifier + assert_equal @str_felix_hex, annotate.revisions[0].author + end + end + + def test_entries_tag + entries1 = @adapter.entries(nil, 'tag01.annotated', + options = {:report_last_commit => true}) + assert entries1 + assert_equal 3, entries1.size + assert_equal 'sources', entries1[1].name + assert_equal 'sources', entries1[1].path + assert_equal 'dir', entries1[1].kind + readme = entries1[2] + assert_equal 'README', readme.name + assert_equal 'README', readme.path + assert_equal 'file', readme.kind + assert_equal 27, readme.size + assert_equal '899a15dba03a3b350b89c3f537e4bbe02a03cdc9', readme.lastrev.identifier + assert_equal Time.gm(2007, 12, 14, 9, 24, 1), readme.lastrev.time + end + + def test_entries_branch + entries1 = @adapter.entries(nil, 'test_branch', + options = {:report_last_commit => true}) + assert entries1 + assert_equal 4, entries1.size + assert_equal 'sources', entries1[1].name + assert_equal 'sources', entries1[1].path + assert_equal 'dir', entries1[1].kind + readme = entries1[2] + assert_equal 'README', readme.name + assert_equal 'README', readme.path + assert_equal 'file', readme.kind + assert_equal 159, readme.size + assert_equal '713f4944648826f558cf548222f813dabe7cbb04', readme.lastrev.identifier + assert_equal Time.gm(2009, 6, 19, 4, 37, 23), readme.lastrev.time + end + + def test_entries_wrong_path_encoding + adpt = Redmine::Scm::Adapters::GitAdapter.new( + REPOSITORY_PATH, + nil, + nil, + nil, + 'EUC-JP' + ) + entries1 = adpt.entries('latin-1-dir', '64f1f3e8') + assert entries1 + assert_equal 3, entries1.size + f1 = entries1[1] + assert_equal nil, f1.name + assert_equal nil, f1.path + assert_equal 'file', f1.kind + end + + def test_entries_latin_1_files + entries1 = @adapter.entries('latin-1-dir', '64f1f3e8') + assert entries1 + assert_equal 3, entries1.size + f1 = entries1[1] + assert_equal "test-#{@char_1}-2.txt", f1.name + assert_equal "latin-1-dir/test-#{@char_1}-2.txt", f1.path + assert_equal 'file', f1.kind + end + + def test_entries_latin_1_dir + if WINDOWS_PASS + puts WINDOWS_SKIP_STR + elsif JRUBY_SKIP + puts JRUBY_SKIP_STR + else + entries1 = @adapter.entries("latin-1-dir/test-#{@char_1}-subdir", + '1ca7f5ed') + assert entries1 + assert_equal 3, entries1.size + f1 = entries1[1] + assert_equal "test-#{@char_1}-2.txt", f1.name + assert_equal "latin-1-dir/test-#{@char_1}-subdir/test-#{@char_1}-2.txt", f1.path + assert_equal 'file', f1.kind + end + end + + def test_entry + entry = @adapter.entry() + assert_equal "", entry.path + assert_equal "dir", entry.kind + entry = @adapter.entry('') + assert_equal "", entry.path + assert_equal "dir", entry.kind + assert_nil @adapter.entry('invalid') + assert_nil @adapter.entry('/invalid') + assert_nil @adapter.entry('/invalid/') + assert_nil @adapter.entry('invalid/invalid') + assert_nil @adapter.entry('invalid/invalid/') + assert_nil @adapter.entry('/invalid/invalid') + assert_nil @adapter.entry('/invalid/invalid/') + ["README", "/README"].each do |path| + entry = @adapter.entry(path, '7234cb2750b63f') + assert_equal "README", entry.path + assert_equal "file", entry.kind + end + ["sources", "/sources", "/sources/"].each do |path| + entry = @adapter.entry(path, '7234cb2750b63f') + assert_equal "sources", entry.path + assert_equal "dir", entry.kind + end + ["sources/watchers_controller.rb", "/sources/watchers_controller.rb"].each do |path| + entry = @adapter.entry(path, '7234cb2750b63f') + assert_equal "sources/watchers_controller.rb", entry.path + assert_equal "file", entry.kind + end + end + + def test_path_encoding_default_utf8 + adpt1 = Redmine::Scm::Adapters::GitAdapter.new( + REPOSITORY_PATH + ) + assert_equal "UTF-8", adpt1.path_encoding + adpt2 = Redmine::Scm::Adapters::GitAdapter.new( + REPOSITORY_PATH, + nil, + nil, + nil, + "" + ) + assert_equal "UTF-8", adpt2.path_encoding + end + + def test_cat_path_invalid + assert_nil @adapter.cat('invalid') + end + + def test_cat_revision_invalid + assert @adapter.cat('README') + assert_nil @adapter.cat('README', '1234abcd5678') + end + + def test_diff_path_invalid + assert_equal [], @adapter.diff('invalid', '713f4944648826f5') + end + + def test_diff_revision_invalid + assert_nil @adapter.diff(nil, '1234abcd5678') + assert_nil @adapter.diff(nil, '713f4944648826f5', '1234abcd5678') + assert_nil @adapter.diff(nil, '1234abcd5678', '713f4944648826f5') + end + + def test_annotate_path_invalid + assert_nil @adapter.annotate('invalid') + end + + def test_annotate_revision_invalid + assert @adapter.annotate('README') + assert_nil @adapter.annotate('README', '1234abcd5678') + end + + private + + def test_scm_version_for(scm_command_version, version) + @adapter.class.expects(:scm_version_from_command_line).returns(scm_command_version) + assert_equal version, @adapter.class.scm_command_version + end + + else + puts "Git test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end + +rescue LoadError + class GitMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/27/27ce56818e709e285d3d63b2d5d116ba119243bf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/27/27ce56818e709e285d3d63b2d5d116ba119243bf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,32 @@ +class ChangeChangesetsRevisionToString < ActiveRecord::Migration + def self.up + # Some backends (eg. SQLServer 2012) do not support changing the type + # of an indexed column so the index needs to be dropped first + # BUT this index is renamed with some backends (at least SQLite3) for + # some (unknown) reasons, thus we check for the other name as well + # so we don't end up with 2 identical indexes + if index_exists? :changesets, [:repository_id, :revision], :name => :changesets_repos_rev + remove_index :changesets, :name => :changesets_repos_rev + end + if index_exists? :changesets, [:repository_id, :revision], :name => :altered_changesets_repos_rev + remove_index :changesets, :name => :altered_changesets_repos_rev + end + + change_column :changesets, :revision, :string, :null => false + + add_index :changesets, [:repository_id, :revision], :unique => true, :name => :changesets_repos_rev + end + + def self.down + if index_exists? :changesets, :changesets_repos_rev + remove_index :changesets, :name => :changesets_repos_rev + end + if index_exists? :changesets, [:repository_id, :revision], :name => :altered_changesets_repos_rev + remove_index :changesets, :name => :altered_changesets_repos_rev + end + + change_column :changesets, :revision, :integer, :null => false + + add_index :changesets, [:repository_id, :revision], :unique => true, :name => :changesets_repos_rev + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/28/28460f7863cf0e308a71ddfcfde8f112f06716f0.svn-base --- a/.svn/pristine/28/28460f7863cf0e308a71ddfcfde8f112f06716f0.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -module ActiveRecord - module Acts - module Tree - def self.included(base) - base.extend(ClassMethods) - end - - # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children - # association. This requires that you have a foreign key column, which by default is called +parent_id+. - # - # class Category < ActiveRecord::Base - # acts_as_tree :order => "name" - # end - # - # Example: - # root - # \_ child1 - # \_ subchild1 - # \_ subchild2 - # - # root = Category.create("name" => "root") - # child1 = root.children.create("name" => "child1") - # subchild1 = child1.children.create("name" => "subchild1") - # - # root.parent # => nil - # child1.parent # => root - # root.children # => [child1] - # root.children.first.children.first # => subchild1 - # - # In addition to the parent and children associations, the following instance methods are added to the class - # after calling acts_as_tree: - # * siblings - Returns all the children of the parent, excluding the current node ([subchild2] when called on subchild1) - # * self_and_siblings - Returns all the children of the parent, including the current node ([subchild1, subchild2] when called on subchild1) - # * ancestors - Returns all the ancestors of the current node ([child1, root] when called on subchild2) - # * root - Returns the root of the current node (root when called on subchild2) - module ClassMethods - # Configuration options are: - # - # * foreign_key - specifies the column name to use for tracking of the tree (default: +parent_id+) - # * order - makes it possible to sort the children according to this SQL snippet. - # * counter_cache - keeps a count in a +children_count+ column if set to +true+ (default: +false+). - def acts_as_tree(options = {}) - configuration = { :foreign_key => "parent_id", :dependent => :destroy, :order => nil, :counter_cache => nil } - configuration.update(options) if options.is_a?(Hash) - - belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache] - has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent] - - class_eval <<-EOV - include ActiveRecord::Acts::Tree::InstanceMethods - - def self.roots - find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) - end - - def self.root - find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) - end - EOV - end - end - - module InstanceMethods - # Returns list of ancestors, starting from parent until root. - # - # subchild1.ancestors # => [child1, root] - def ancestors - node, nodes = self, [] - nodes << node = node.parent while node.parent - nodes - end - - # Returns list of descendants. - # - # root.descendants # => [child1, subchild1, subchild2] - def descendants(depth=nil) - depth ||= 0 - result = children.dup - unless depth == 1 - result += children.collect {|child| child.descendants(depth-1)}.flatten - end - result - end - - # Returns list of descendants and a reference to the current node. - # - # root.self_and_descendants # => [root, child1, subchild1, subchild2] - def self_and_descendants(depth=nil) - [self] + descendants(depth) - end - - # Returns the root node of the tree. - def root - node = self - node = node.parent while node.parent - node - end - - # Returns all siblings of the current node. - # - # subchild1.siblings # => [subchild2] - def siblings - self_and_siblings - [self] - end - - # Returns all siblings and a reference to the current node. - # - # subchild1.self_and_siblings # => [subchild1, subchild2] - def self_and_siblings - parent ? parent.children : self.class.roots - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/28/284f5f782295ac4c8fe3dbacfe87f5903171e538.svn-base --- a/.svn/pristine/28/284f5f782295ac4c8fe3dbacfe87f5903171e538.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,126 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ApiTest::IssueCategoriesTest < ActionController::IntegrationTest - fixtures :projects, :users, :issue_categories, :issues, - :roles, - :member_roles, - :members, - :enabled_modules - - def setup - Setting.rest_api_enabled = '1' - end - - context "GET /projects/:project_id/issue_categories.xml" do - should "return issue categories" do - get '/projects/1/issue_categories.xml', {}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'issue_categories', - :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}} - end - end - - context "GET /issue_categories/2.xml" do - should "return requested issue category" do - get '/issue_categories/2.xml', {}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'issue_category', - :child => {:tag => 'id', :content => '2'} - end - end - - context "POST /projects/:project_id/issue_categories.xml" do - should "return create issue category" do - assert_difference 'IssueCategory.count' do - post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith') - end - assert_response :created - assert_equal 'application/xml', @response.content_type - - category = IssueCategory.first(:order => 'id DESC') - assert_equal 'API', category.name - assert_equal 1, category.project_id - end - - context "with invalid parameters" do - should "return errors" do - assert_no_difference 'IssueCategory.count' do - post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith') - end - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - - assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} - end - end - end - - context "PUT /issue_categories/2.xml" do - context "with valid parameters" do - should "update issue category" do - assert_no_difference 'IssueCategory.count' do - put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith') - end - assert_response :ok - assert_equal '', @response.body - assert_equal 'API Update', IssueCategory.find(2).name - end - end - - context "with invalid parameters" do - should "return errors" do - assert_no_difference 'IssueCategory.count' do - put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith') - end - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - - assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} - end - end - end - - context "DELETE /issue_categories/1.xml" do - should "destroy issue categories" do - assert_difference 'IssueCategory.count', -1 do - delete '/issue_categories/1.xml', {}, credentials('jsmith') - end - assert_response :ok - assert_equal '', @response.body - assert_nil IssueCategory.find_by_id(1) - end - - should "reassign issues with :reassign_to_id param" do - issue_count = Issue.count(:conditions => {:category_id => 1}) - assert issue_count > 0 - - assert_difference 'IssueCategory.count', -1 do - assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do - delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith') - end - end - assert_response :ok - assert_equal '', @response.body - assert_nil IssueCategory.find_by_id(1) - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/28/287ad425b706a1e849a16749d3a013cf82f1d342.svn-base --- a/.svn/pristine/28/287ad425b706a1e849a16749d3a013cf82f1d342.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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::Utils::DateCalculationTest < ActiveSupport::TestCase - include Redmine::Utils::DateCalculation - - def test_working_days_without_non_working_week_days - with_settings :non_working_week_days => [] do - assert_working_days 18, '2012-10-09', '2012-10-27' - assert_working_days 6, '2012-10-09', '2012-10-15' - assert_working_days 5, '2012-10-09', '2012-10-14' - assert_working_days 3, '2012-10-09', '2012-10-12' - assert_working_days 3, '2012-10-14', '2012-10-17' - assert_working_days 16, '2012-10-14', '2012-10-30' - end - end - - def test_working_days_with_non_working_week_days - with_settings :non_working_week_days => %w(6 7) do - assert_working_days 14, '2012-10-09', '2012-10-27' - assert_working_days 4, '2012-10-09', '2012-10-15' - assert_working_days 4, '2012-10-09', '2012-10-14' - assert_working_days 3, '2012-10-09', '2012-10-12' - assert_working_days 8, '2012-10-09', '2012-10-19' - assert_working_days 8, '2012-10-11', '2012-10-23' - assert_working_days 2, '2012-10-14', '2012-10-17' - assert_working_days 11, '2012-10-14', '2012-10-30' - end - end - - def test_add_working_days_without_non_working_week_days - with_settings :non_working_week_days => [] do - assert_add_working_days '2012-10-10', '2012-10-10', 0 - assert_add_working_days '2012-10-11', '2012-10-10', 1 - assert_add_working_days '2012-10-12', '2012-10-10', 2 - assert_add_working_days '2012-10-13', '2012-10-10', 3 - assert_add_working_days '2012-10-25', '2012-10-10', 15 - end - end - - def test_add_working_days_with_non_working_week_days - with_settings :non_working_week_days => %w(6 7) do - assert_add_working_days '2012-10-10', '2012-10-10', 0 - assert_add_working_days '2012-10-11', '2012-10-10', 1 - assert_add_working_days '2012-10-12', '2012-10-10', 2 - assert_add_working_days '2012-10-15', '2012-10-10', 3 - assert_add_working_days '2012-10-31', '2012-10-10', 15 - assert_add_working_days '2012-10-19', '2012-10-09', 8 - assert_add_working_days '2012-10-23', '2012-10-11', 8 - end - end - - def assert_working_days(expected_days, from, to) - assert_equal expected_days, working_days(from.to_date, to.to_date) - end - - def assert_add_working_days(expected_date, from, working_days) - assert_equal expected_date.to_date, add_working_days(from.to_date, working_days) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/28/287b739e36a269ed72c54dcd10ebdefa5f23d6ee.svn-base --- a/.svn/pristine/28/287b739e36a269ed72c54dcd10ebdefa5f23d6ee.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,173 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'iconv' -require 'net/ldap' -require 'net/ldap/dn' -require 'timeout' - -class AuthSourceLdap < AuthSource - validates_presence_of :host, :port, :attr_login - validates_length_of :name, :host, :maximum => 60, :allow_nil => true - validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true - validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true - validates_numericality_of :port, :only_integer => true - validates_numericality_of :timeout, :only_integer => true, :allow_blank => true - validate :validate_filter - - before_validation :strip_ldap_attributes - - def initialize(attributes=nil, *args) - super - self.port = 389 if self.port == 0 - end - - def authenticate(login, password) - return nil if login.blank? || password.blank? - - with_timeout do - attrs = get_user_dn(login, password) - if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) - logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? - return attrs.except(:dn) - end - end - rescue Net::LDAP::LdapError => e - raise AuthSourceException.new(e.message) - end - - # test the connection to the LDAP - def test_connection - with_timeout do - ldap_con = initialize_ldap_con(self.account, self.account_password) - ldap_con.open { } - end - rescue Net::LDAP::LdapError => e - raise AuthSourceException.new(e.message) - end - - def auth_method_name - "LDAP" - end - - private - - def with_timeout(&block) - timeout = self.timeout - timeout = 20 unless timeout && timeout > 0 - Timeout.timeout(timeout) do - return yield - end - rescue Timeout::Error => e - raise AuthSourceTimeoutException.new(e.message) - end - - def ldap_filter - if filter.present? - Net::LDAP::Filter.construct(filter) - end - rescue Net::LDAP::LdapError - nil - end - - def validate_filter - if filter.present? && ldap_filter.nil? - errors.add(:filter, :invalid) - end - end - - def strip_ldap_attributes - [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr| - write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil? - end - end - - def initialize_ldap_con(ldap_user, ldap_password) - options = { :host => self.host, - :port => self.port, - :encryption => (self.tls ? :simple_tls : nil) - } - options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank? - Net::LDAP.new options - end - - def get_user_attributes_from_ldap_entry(entry) - { - :dn => entry.dn, - :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname), - :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname), - :mail => AuthSourceLdap.get_attr(entry, self.attr_mail), - :auth_source_id => self.id - } - end - - # Return the attributes needed for the LDAP search. It will only - # include the user attributes if on-the-fly registration is enabled - def search_attributes - if onthefly_register? - ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] - else - ['dn'] - end - end - - # Check if a DN (user record) authenticates with the password - def authenticate_dn(dn, password) - if dn.present? && password.present? - initialize_ldap_con(dn, password).bind - end - end - - # Get the user's dn and any attributes for them, given their login - def get_user_dn(login, password) - ldap_con = nil - if self.account && self.account.include?("$login") - ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password) - else - ldap_con = initialize_ldap_con(self.account, self.account_password) - end - login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) - object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) - attrs = {} - - search_filter = object_filter & login_filter - if f = ldap_filter - search_filter = search_filter & f - end - - ldap_con.search( :base => self.base_dn, - :filter => search_filter, - :attributes=> search_attributes) do |entry| - - if onthefly_register? - attrs = get_user_attributes_from_ldap_entry(entry) - else - attrs = {:dn => entry.dn} - end - - logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug? - end - - attrs - end - - def self.get_attr(entry, attr_name) - if !attr_name.blank? - entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/28/28a718992fc9b78318367f1ea258610932c7f8f4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/28a718992fc9b78318367f1ea258610932c7f8f4.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,392 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 AccountControllerTest < ActionController::TestCase + fixtures :users, :roles + + def setup + User.current = nil + end + + def test_get_login + get :login + assert_response :success + assert_template 'login' + + assert_select 'input[name=username]' + assert_select 'input[name=password]' + end + + def test_get_login_while_logged_in_should_redirect_to_home + @request.session[:user_id] = 2 + + get :login + assert_redirected_to '/' + assert_equal 2, @request.session[:user_id] + end + + def test_login_should_redirect_to_back_url_param + # request.uri is "test.host" in test environment + back_urls = [ + 'http://test.host/issues/show/1', + '/' + ] + back_urls.each do |back_url| + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url + assert_redirected_to back_url + end + end + + def test_login_with_suburi_should_redirect_to_back_url_param + @relative_url_root = ApplicationController.relative_url_root + ApplicationController.relative_url_root = '/redmine' + + back_urls = [ + 'http://test.host/redmine/issues/show/1', + '/redmine' + ] + back_urls.each do |back_url| + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url + assert_redirected_to back_url + end + ensure + ApplicationController.relative_url_root = @relative_url_root + end + + def test_login_should_not_redirect_to_another_host + back_urls = [ + 'http://test.foo/fake', + '//test.foo/fake' + ] + back_urls.each do |back_url| + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url + assert_redirected_to '/my/page' + end + end + + def test_login_with_suburi_should_not_redirect_to_another_suburi + @relative_url_root = ApplicationController.relative_url_root + ApplicationController.relative_url_root = '/redmine' + + back_urls = [ + 'http://test.host/', + 'http://test.host/fake', + 'http://test.host/fake/issues', + 'http://test.host/redmine/../fake', + 'http://test.host/redmine/../fake/issues', + 'http://test.host/redmine/%2e%2e/fake' + ] + back_urls.each do |back_url| + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => back_url + assert_redirected_to '/my/page' + end + ensure + ApplicationController.relative_url_root = @relative_url_root + end + + def test_login_with_wrong_password + post :login, :username => 'admin', :password => 'bad' + assert_response :success + assert_template 'login' + + assert_select 'div.flash.error', :text => /Invalid user or password/ + assert_select 'input[name=username][value=admin]' + assert_select 'input[name=password]' + assert_select 'input[name=password][value]', 0 + end + + def test_login_with_locked_account_should_fail + User.find(2).update_attribute :status, User::STATUS_LOCKED + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/login' + assert_include 'locked', flash[:error] + assert_nil @request.session[:user_id] + end + + def test_login_as_registered_user_with_manual_activation_should_inform_user + User.find(2).update_attribute :status, User::STATUS_REGISTERED + + with_settings :self_registration => '2', :default_language => 'en' do + post :login, :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/login' + assert_include 'pending administrator approval', flash[:error] + end + end + + def test_login_as_registered_user_with_email_activation_should_propose_new_activation_email + User.find(2).update_attribute :status, User::STATUS_REGISTERED + + with_settings :self_registration => '1', :default_language => 'en' do + post :login, :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/login' + assert_equal 2, @request.session[:registered_user_id] + assert_include 'new activation email', flash[:error] + end + end + + def test_login_should_rescue_auth_source_exception + source = AuthSource.create!(:name => 'Test') + User.find(2).update_attribute :auth_source_id, source.id + AuthSource.any_instance.stubs(:authenticate).raises(AuthSourceException.new("Something wrong")) + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 500 + assert_error_tag :content => /Something wrong/ + end + + def test_login_should_reset_session + @controller.expects(:reset_session).once + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 302 + end + + def test_get_logout_should_not_logout + @request.session[:user_id] = 2 + get :logout + assert_response :success + assert_template 'logout' + + assert_equal 2, @request.session[:user_id] + end + + def test_get_logout_with_anonymous_should_redirect + get :logout + assert_redirected_to '/' + end + + def test_logout + @request.session[:user_id] = 2 + post :logout + assert_redirected_to '/' + assert_nil @request.session[:user_id] + end + + def test_logout_should_reset_session + @controller.expects(:reset_session).once + + @request.session[:user_id] = 2 + post :logout + assert_response 302 + end + + def test_get_register_with_registration_on + with_settings :self_registration => '3' do + get :register + assert_response :success + assert_template 'register' + assert_not_nil assigns(:user) + + assert_select 'input[name=?]', 'user[password]' + assert_select 'input[name=?]', 'user[password_confirmation]' + end + end + + def test_get_register_should_detect_user_language + with_settings :self_registration => '3' do + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + get :register + assert_response :success + assert_not_nil assigns(:user) + assert_equal 'fr', assigns(:user).language + assert_select 'select[name=?]', 'user[language]' do + assert_select 'option[value=fr][selected=selected]' + end + end + end + + def test_get_register_with_registration_off_should_redirect + with_settings :self_registration => '0' do + get :register + assert_redirected_to '/' + end + end + + # See integration/account_test.rb for the full test + def test_post_register_with_registration_on + with_settings :self_registration => '3' do + assert_difference 'User.count' do + post :register, :user => { + :login => 'register', + :password => 'secret123', + :password_confirmation => 'secret123', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + assert_redirected_to '/my/account' + end + user = User.first(:order => 'id DESC') + assert_equal 'register', user.login + assert_equal 'John', user.firstname + assert_equal 'Doe', user.lastname + assert_equal 'register@example.com', user.mail + assert user.check_password?('secret123') + assert user.active? + end + end + + def test_post_register_with_registration_off_should_redirect + with_settings :self_registration => '0' do + assert_no_difference 'User.count' do + post :register, :user => { + :login => 'register', + :password => 'test', + :password_confirmation => 'test', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + assert_redirected_to '/' + end + end + end + + def test_get_lost_password_should_display_lost_password_form + get :lost_password + assert_response :success + assert_select 'input[name=mail]' + end + + def test_lost_password_for_active_user_should_create_a_token + Token.delete_all + ActionMailer::Base.deliveries.clear + assert_difference 'ActionMailer::Base.deliveries.size' do + assert_difference 'Token.count' do + with_settings :host_name => 'mydomain.foo', :protocol => 'http' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_redirected_to '/login' + end + end + end + + token = Token.order('id DESC').first + assert_equal User.find(2), token.user + assert_equal 'recovery', token.action + + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/account/lost_password?token=#{token.value}" + end + end + + def test_lost_password_for_unknown_user_should_fail + Token.delete_all + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'invalid@somenet.foo' + assert_response :success + end + end + + def test_lost_password_for_non_active_user_should_fail + Token.delete_all + assert User.find(2).lock! + + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_redirected_to '/account/lost_password' + end + end + + def test_lost_password_for_user_who_cannot_change_password_should_fail + User.any_instance.stubs(:change_password_allowed?).returns(false) + + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_response :success + end + end + + def test_get_lost_password_with_token_should_display_the_password_recovery_form + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + get :lost_password, :token => token.value + assert_response :success + assert_template 'password_recovery' + + assert_select 'input[type=hidden][name=token][value=?]', token.value + end + + def test_get_lost_password_with_invalid_token_should_redirect + get :lost_password, :token => "abcdef" + assert_redirected_to '/' + end + + def test_post_lost_password_with_token_should_change_the_user_password + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to '/login' + user.reload + assert user.check_password?('newpass123') + assert_nil Token.find_by_id(token.id), "Token was not deleted" + end + + def test_post_lost_password_with_token_for_non_active_user_should_fail + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + user.lock! + + post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to '/' + assert ! user.check_password?('newpass123') + end + + def test_post_lost_password_with_token_and_password_confirmation_failure_should_redisplay_the_form + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + post :lost_password, :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'wrongpass' + assert_response :success + assert_template 'password_recovery' + assert_not_nil Token.find_by_id(token.id), "Token was deleted" + + assert_select 'input[type=hidden][name=token][value=?]', token.value + end + + def test_post_lost_password_with_invalid_token_should_redirect + post :lost_password, :token => "abcdef", :new_password => 'newpass', :new_password_confirmation => 'newpass' + assert_redirected_to '/' + end + + def test_activation_email_should_send_an_activation_email + User.find(2).update_attribute :status, User::STATUS_REGISTERED + @request.session[:registered_user_id] = 2 + + with_settings :self_registration => '1' do + assert_difference 'ActionMailer::Base.deliveries.size' do + get :activation_email + assert_redirected_to '/login' + end + end + end + + def test_activation_email_without_session_data_should_fail + User.find(2).update_attribute :status, User::STATUS_REGISTERED + + with_settings :self_registration => '1' do + assert_no_difference 'ActionMailer::Base.deliveries.size' do + get :activation_email + assert_redirected_to '/' + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/28/28c1e15c05f8a765c8f89dc91afdd6d16aa83f7d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/28c1e15c05f8a765c8f89dc91afdd6d16aa83f7d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1116 @@ +# Estonian localization for Redmine +# Copyright (C) 2012 Kaitseministeerium +# +# 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. + +et: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + 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: "%d.%m.%Y" + short: "%d.%b" + long: "%d. %B %Y" + + day_names: [Pühapäev, Esmaspäev, Teisipäev, Kolmapäev, Neljapäev, Reede, Laupäev] + abbr_day_names: [P, E, T, K, N, R, L] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Jaanuar, Veebruar, Märts, Aprill, Mai, Juuni, Juuli, August, September, Oktoober, November, Detsember] + abbr_month_names: [~, jaan, veebr, märts, apr, mai, juuni, juuli, aug, sept, okt, nov, dets] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%d.%m.%Y %H:%M" + time: "%H:%M" + short: "%d.%b %H:%M" + long: "%d. %B %Y %H:%M %z" + am: "enne lõunat" + pm: "peale lõunat" + + datetime: + distance_in_words: + half_a_minute: "pool minutit" + less_than_x_seconds: + one: "vähem kui sekund" + other: "vähem kui %{count} sekundit" + x_seconds: + one: "1 sekund" + other: "%{count} sekundit" + less_than_x_minutes: + one: "vähem kui minut" + other: "vähem kui %{count} minutit" + x_minutes: + one: "1 minut" + other: "%{count} minutit" + about_x_hours: + one: "umbes tund" + other: "umbes %{count} tundi" + x_hours: + one: "1 tund" + other: "%{count} tundi" + x_days: + one: "1 päev" + other: "%{count} päeva" + about_x_months: + one: "umbes kuu" + other: "umbes %{count} kuud" + x_months: + one: "1 kuu" + other: "%{count} kuud" + about_x_years: + one: "umbes aasta" + other: "umbes %{count} aastat" + over_x_years: + one: "rohkem kui aasta" + other: "rohkem kui %{count} aastat" + almost_x_years: + one: "peaaegu aasta" + other: "peaaegu %{count} aastat" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "bait" + other: "baiti" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "ja" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 viga ei võimaldanud selle %{model} salvestamist" + other: "%{count} viga ei võimaldanud selle %{model} salvestamist" + messages: + inclusion: "ei ole nimekirjas" + exclusion: "on reserveeritud" + invalid: "ei sobi" + confirmation: "ei lange kinnitusega kokku" + accepted: "peab olema aktsepteeritud" + empty: "ei või olla tühi" + blank: "ei või olla täitmata" + too_long: "on liiga pikk (lubatud on kuni %{count} märki)" + too_short: "on liiga lühike (vaja on vähemalt %{count} märki)" + wrong_length: "on vale pikkusega (peaks olema %{count} märki)" + taken: "on juba võetud" + not_a_number: "ei ole arv" + not_a_date: "ei ole korrektne kuupäev" + greater_than: "peab olema suurem kui %{count}" + greater_than_or_equal_to: "peab olema võrdne või suurem kui %{count}" + equal_to: "peab võrduma %{count}-ga" + less_than: "peab olema väiksem kui %{count}" + less_than_or_equal_to: "peab olema võrdne või väiksem kui %{count}" + odd: "peab olema paaritu arv" + even: "peab olema paarisarv" + greater_than_start_date: "peab olema suurem kui alguskuupäev" + not_same_project: "ei kuulu sama projekti juurde" + circular_dependency: "See suhe looks vastastikuse sõltuvuse" + cant_link_an_issue_with_a_descendant: "Teemat ei saa sidustada tema enda alamteemaga" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: "Palun vali" + + general_text_No: "Ei" + general_text_Yes: "Jah" + general_text_no: "ei" + general_text_yes: "jah" + general_lang_name: "Eesti" + general_csv_separator: "," + general_csv_decimal_separator: "." + general_csv_encoding: ISO-8859-13 + general_pdf_encoding: UTF-8 + general_first_day_of_week: "1" + + notice_account_updated: "Konto uuendamine õnnestus." + notice_account_invalid_creditentials: "Sobimatu kasutajanimi või parool" + notice_account_password_updated: "Parooli uuendamine õnnestus." + notice_account_wrong_password: "Vale parool" + notice_account_register_done: "Konto loomine õnnestus. Konto aktiveerimiseks vajuta vastaval lingil Sulle saadetud e-kirjas." + notice_account_unknown_email: "Tundmatu kasutaja." + notice_can_t_change_password: "See konto kasutab välist autentimisallikat. Siin ei saa selle konto parooli vahetada." + notice_account_lost_email_sent: "Sulle saadeti e-kiri parooli vahetamise juhistega." + notice_account_activated: "Su konto on aktiveeritud. Saad nüüd sisse logida." + notice_successful_create: "Loomine õnnestus." + notice_successful_update: "Uuendamine õnnestus." + notice_successful_delete: "Kustutamine õnnestus." + notice_successful_connection: "Ühenduse loomine õnnestus." + notice_file_not_found: "Sellist lehte ei leitud." + notice_locking_conflict: "Teine kasutaja uuendas vahepeal neid andmeid." + notice_not_authorized: "Sul ei ole sellele lehele ligipääsuks õigusi." + notice_not_authorized_archived_project: "See projekt on arhiveeritud." + notice_email_sent: "%{value}-le saadeti kiri" + notice_email_error: "Kirja saatmisel tekkis viga (%{value})" + notice_feeds_access_key_reseted: "Sinu Atom juurdepääsuvõti nulliti." + notice_api_access_key_reseted: "Sinu API juurdepääsuvõti nulliti." + notice_failed_to_save_issues: "%{count} teemat %{total}-st ei õnnestunud salvestada: %{ids}." + notice_failed_to_save_time_entries: "%{count} ajakulu kannet %{total}-st ei õnnestunud salvestada: %{ids}." + notice_failed_to_save_members: "Liiget/liikmeid ei õnnestunud salvestada: %{errors}." + notice_no_issue_selected: "Ühtegi teemat ei ole valitud! Palun vali teema(d), mida soovid muuta." + notice_account_pending: "Sinu konto on loodud ja ootab nüüd administraatori kinnitust." + notice_default_data_loaded: "Algseadistuste laadimine õnnestus." + notice_unable_delete_version: "Versiooni kustutamine ei õnnestunud." + notice_unable_delete_time_entry: "Ajakulu kande kustutamine ei õnnestunud." + notice_issue_done_ratios_updated: "Teema edenemise astmed on uuendatud." + notice_gantt_chart_truncated: "Diagrammi kärbiti kuna ületati kuvatavate objektide suurim hulk (%{max})" + notice_issue_successful_create: "Teema %{id} loodud." + notice_issue_update_conflict: "Teine kasutaja uuendas seda teemat Sinuga samaaegselt." + notice_account_deleted: "Sinu konto on lõplikult kustutatud." + + error_can_t_load_default_data: "Algseadistusi ei saanud laadida: %{value}" + error_scm_not_found: "Seda sissekannet hoidlast ei leitud." + error_scm_command_failed: "Hoidla poole pöördumisel tekkis viga: %{value}" + error_scm_annotate: "Sissekannet ei eksisteeri või ei saa annoteerida." + error_scm_annotate_big_text_file: "Sissekannet ei saa annoteerida, kuna see on liiga pikk." + error_issue_not_found_in_project: "Teemat ei leitud või see ei kuulu siia projekti" + error_no_tracker_in_project: "Selle projektiga ei ole seostatud ühtegi valdkonda. Palun vaata üle projekti seaded." + error_no_default_issue_status: 'Teema algolek on määramata. Palun vaata asetused üle ("Seadistused -> Olekud").' + error_can_not_delete_custom_field: "Omaloodud välja kustutamine ei õnnestunud" + error_can_not_delete_tracker: "See valdkond on mõnes teemas kasutusel ja seda ei saa kustutada." + error_can_not_remove_role: "See roll on mõnes projektis kasutusel ja seda ei saa kustutada." + error_can_not_reopen_issue_on_closed_version: "Suletud versiooni juurde kuulunud teemat ei saa taasavada" + error_can_not_archive_project: "Seda projekti ei saa arhiveerida" + error_issue_done_ratios_not_updated: "Teema edenemise astmed jäid uuendamata." + error_workflow_copy_source: "Palun vali algne valdkond või roll" + error_workflow_copy_target: "Palun vali sihtvaldkon(na)d või -roll(id)" + error_unable_delete_issue_status: "Oleku kustutamine ei õnnestunud" + error_unable_to_connect: "Ühenduse loomine ei õnnestunud (%{value})" + error_attachment_too_big: "Faili ei saa üles laadida, sest see on lubatust (%{max_size}) pikem" + warning_attachments_not_saved: "%{count} faili salvestamine ei õnnestunud." + + mail_subject_lost_password: "Sinu %{value} parool" + mail_body_lost_password: "Et vahetada oma parooli, vajuta järgmisele lingile:" + mail_subject_register: "Sinu %{value} konto aktiveerimine" + mail_body_register: "Et aktiveerida oma kontot, vajuta järgmisele lingile:" + mail_body_account_information_external: "Sisse logimiseks saad kasutada oma %{value} kontot." + mail_body_account_information: "Sinu konto teave" + mail_subject_account_activation_request: "%{value} konto aktiveerimise nõue" + mail_body_account_activation_request: "Registreerus uus kasutaja (%{value}). Konto avamine ootab Sinu kinnitust:" + mail_subject_reminder: "%{count} teema tähtaeg jõuab kätte järgmise %{days} päeva jooksul" + mail_body_reminder: "%{count} Sulle määratud teema tähtaeg jõuab kätte järgmise %{days} päeva jooksul:" + mail_subject_wiki_content_added: "Lisati '%{id}' vikileht" + mail_body_wiki_content_added: "'%{id}' vikileht lisati %{author} poolt." + mail_subject_wiki_content_updated: "Uuendati '%{id}' vikilehte" + mail_body_wiki_content_updated: "'%{id}' vikilehte uuendati %{author} poolt." + + + field_name: "Nimi" + field_description: "Kirjeldus" + field_summary: "Kokkuvõte" + field_is_required: "Kohustuslik" + field_firstname: "Eesnimi" + field_lastname: "Perekonnanimi" + field_mail: "E-post" + field_filename: "Fail" + field_filesize: "Pikkus" + field_downloads: "Allalaadimist" + field_author: "Autor" + field_created_on: "Loodud" + field_updated_on: "Uuendatud" + field_field_format: "Tüüp" + field_is_for_all: "Kõigile projektidele" + field_possible_values: "Võimalikud väärtused" + field_regexp: "Regulaarne avaldis" + field_min_length: "Vähim pikkus" + field_max_length: "Suurim pikkus" + field_value: "Väärtus" + field_category: "Kategooria" + field_title: "Pealkiri" + field_project: "Projekt" + field_issue: "Teema" + field_status: "Olek" + field_notes: "Märkused" + field_is_closed: "Sulgeb teema" + field_is_default: "Algolek" + field_tracker: "Valdkond" + field_subject: "Teema" + field_due_date: "Tähtaeg" + field_assigned_to: "Tegeleja" + field_priority: "Prioriteet" + field_fixed_version: "Sihtversioon" + field_user: "Kasutaja" + field_principal: "Vastutav isik" + field_role: "Roll" + field_homepage: "Koduleht" + field_is_public: "Avalik" + field_parent: "Emaprojekt" + field_is_in_roadmap: "Teemad on teekaardil näha" + field_login: "Kasutajanimi" + field_mail_notification: "Teated e-kirjaga" + field_admin: "Admin" + field_last_login_on: "Viimane ühendus" + field_language: "Keel" + field_effective_date: "Tähtaeg" + field_password: "Parool" + field_new_password: "Uus parool" + field_password_confirmation: "Kinnitus" + field_version: "Versioon" + field_type: "Tüüp" + field_host: "Server" + field_port: "Port" + field_account: "Konto" + field_base_dn: "Baas DN" + field_attr_login: "Kasutajanime atribuut" + field_attr_firstname: "Eesnime atribuut" + field_attr_lastname: "Perekonnanime atribuut" + field_attr_mail: "E-posti atribuut" + field_onthefly: "Kasutaja automaatne loomine" + field_start_date: "Alguskuupäev" + field_done_ratio: "% tehtud" + field_auth_source: "Autentimise viis" + field_hide_mail: "Peida e-posti aadress" + field_comments: "Kommentaar" + field_url: "URL" + field_start_page: "Esileht" + field_subproject: "Alamprojekt" + field_hours: "tundi" + field_activity: "Tegevus" + field_spent_on: "Kuupäev" + field_identifier: "Tunnus" + field_is_filter: "Kasutatakse filtrina" + field_issue_to: "Seotud teema" + field_delay: "Viivitus" + field_assignable: "Saab määrata teemadega tegelema" + field_redirect_existing_links: "Suuna olemasolevad lingid ringi" + field_estimated_hours: "Eeldatav ajakulu" + field_column_names: "Veerud" + field_time_entries: "Ajakulu" + field_time_zone: "Ajatsoon" + field_searchable: "Otsitav" + field_default_value: "Vaikimisi" + field_comments_sorting: "Kommentaaride järjestus" + field_parent_title: "Pärineb lehest" + field_editable: "Muudetav" + field_watcher: "Jälgija" + field_identity_url: "OpenID URL" + field_content: "Sisu" + field_group_by: "Grupeeri tulemus" + field_sharing: "Teemade jagamine" + field_parent_issue: "Pärineb teemast" + field_member_of_group: "Tegeleja grupp" + field_assigned_to_role: "Tegeleja roll" + field_text: "Tekstiväli" + field_visible: "Nähtav" + field_warn_on_leaving_unsaved: "Hoiata salvestamata sisuga lehtedelt lahkumisel" + field_issues_visibility: "See roll näeb" + field_is_private: "Privaatne" + field_commit_logs_encoding: "Sissekannete kodeering" + field_scm_path_encoding: "Teeraja märkide kodeering" + field_path_to_repository: "Hoidla teerada" + field_root_directory: "Juurkataloog" + field_cvsroot: "CVSROOT" + field_cvs_module: "Moodul" + field_repository_is_default: "Peamine hoidla" + field_multiple: "Korraga mitu väärtust" + field_auth_source_ldap_filter: "LDAP filter" + + setting_app_title: "Veebilehe pealkiri" + setting_app_subtitle: "Veebilehe alampealkiri" + setting_welcome_text: "Tervitustekst" + setting_default_language: "Vaikimisi keel" + setting_login_required: "Autentimine kohustuslik" + setting_self_registration: "Omaloodud konto aktiveerimine" + setting_attachment_max_size: "Manuse suurim pikkus" + setting_issues_export_limit: "Teemade ekspordi limiit" + setting_mail_from: "Saatja e-posti aadress" + setting_bcc_recipients: "Saajaid ei näidata (lähevad BCC reale)" + setting_plain_text_mail: "E-kiri tavalise tekstina (ilma HTML-ta)" + setting_host_name: "Serveri nimi ja teerada" + setting_text_formatting: "Vormindamise abi" + setting_wiki_compression: "Viki ajaloo pakkimine" + setting_feeds_limit: "Atom voogude suurim objektide arv" + setting_default_projects_public: "Uued projektid on vaikimisi avalikud" + setting_autofetch_changesets: "Lae uuendused automaatselt" + setting_sys_api_enabled: "Hoidlate haldamine veebiteenuse kaudu" + setting_commit_ref_keywords: "Viitade võtmesõnad" + setting_commit_fix_keywords: "Paranduste võtmesõnad" + setting_autologin: "Automaatne sisselogimine" + setting_date_format: "Kuupäevaformaat" + setting_time_format: "Ajaformaat" + setting_cross_project_issue_relations: "Luba siduda eri projektide teemasid" + setting_issue_list_default_columns: "Teemade nimekirja vaikimisi veerud" + setting_repositories_encodings: "Manuste ja hoidlate kodeering" + setting_emails_header: "E-kirja päis" + setting_emails_footer: "E-kirja jalus" + setting_protocol: "Protokoll" + setting_per_page_options: "Objekte lehe kohta variandid" + setting_user_format: "Kasutaja nime esitamise vorm" + setting_activity_days_default: "Projektide ajalugu näidatakse" + setting_display_subprojects_issues: "Näita projektis vaikimisi ka alamprojektide teemasid" + setting_enabled_scm: "Kasutatavad lähtekoodi haldusvahendid" + setting_mail_handler_body_delimiters: "Kärbi e-kirja lõpp peale sellist rida" + setting_mail_handler_api_enabled: "E-kirjade vastuvõtt veebiteenuse kaudu" + setting_mail_handler_api_key: "Veebiteenuse API võti" + setting_sequential_project_identifiers: "Genereeri järjestikused projektitunnused" + setting_gravatar_enabled: "Kasuta Gravatari kasutajaikoone" + setting_gravatar_default: "Vaikimisi kasutatav ikoon" + setting_diff_max_lines_displayed: "Enim korraga näidatavaid erinevusi" + setting_file_max_size_displayed: "Kuvatava tekstifaili suurim pikkus" + setting_repository_log_display_limit: "Enim ajaloos näidatavaid sissekandeid" + setting_openid: "Luba OpenID-ga registreerimine ja sisselogimine" + setting_password_min_length: "Lühima lubatud parooli pikkus" + setting_new_project_user_role_id: "Projekti looja roll oma projektis" + setting_default_projects_modules: "Vaikimisi moodulid uutes projektides" + setting_issue_done_ratio: "Määra teema edenemise aste" + setting_issue_done_ratio_issue_field: "kasutades vastavat välja" + setting_issue_done_ratio_issue_status: "kasutades teema olekut" + setting_start_of_week: "Nädala alguspäev" + setting_rest_api_enabled: "Luba REST API kasutamine" + setting_cache_formatted_text: "Puhverda vormindatud teksti" + setting_default_notification_option: "Vaikimisi teavitatakse" + setting_commit_logtime_enabled: "Luba ajakulu sisestamine" + setting_commit_logtime_activity_id: "Tegevus kulunud ajal" + setting_gantt_items_limit: "Gantti diagrammi objektide suurim hulk" + setting_issue_group_assignment: "Luba teemade andmine gruppidele" + setting_default_issue_start_date_to_creation_date: "Uute teemade alguskuupäevaks teema loomise päev" + setting_commit_cross_project_ref: "Luba viiteid ja parandusi ka kõigi teiste projektide teemadele" + setting_unsubscribe: "Luba kasutajal oma konto kustutada" + + permission_add_project: "Projekte luua" + permission_add_subprojects: "Alamprojekte luua" + permission_edit_project: "Projekte muuta" + permission_select_project_modules: "Projektimooduleid valida" + permission_manage_members: "Liikmeid hallata" + permission_manage_project_activities: "Projekti tegevusi hallata" + permission_manage_versions: "Versioone hallata" + permission_manage_categories: "Kategooriaid hallata" + permission_view_issues: "Teemasid näha" + permission_add_issues: "Teemasid lisada" + permission_edit_issues: "Teemasid uuendada" + permission_manage_issue_relations: "Teemade seoseid hallata" + permission_set_issues_private: "Teemasid avalikeks või privaatseiks seada" + permission_set_own_issues_private: "Omi teemasid avalikeks või privaatseiks seada" + permission_add_issue_notes: "Märkusi lisada" + permission_edit_issue_notes: "Märkusi muuta" + permission_edit_own_issue_notes: "Omi märkusi muuta" + permission_move_issues: "Teemasid teise projekti tõsta" + permission_delete_issues: "Teemasid kustutada" + permission_manage_public_queries: "Avalikke päringuid hallata" + permission_save_queries: "Päringuid salvestada" + permission_view_gantt: "Gantti diagramme näha" + permission_view_calendar: "Kalendrit näha" + permission_view_issue_watchers: "Jälgijate nimekirja näha" + permission_add_issue_watchers: "Jälgijaid lisada" + permission_delete_issue_watchers: "Jälgijaid kustutada" + permission_log_time: "Ajakulu sisestada" + permission_view_time_entries: "Ajakulu näha" + permission_edit_time_entries: "Ajakulu kandeid muuta" + permission_edit_own_time_entries: "Omi ajakulu kandeid muuta" + permission_manage_news: "Uudiseid hallata" + permission_comment_news: "Uudiseid kommenteerida" + permission_view_documents: "Dokumente näha" + permission_manage_files: "Faile hallata" + permission_view_files: "Faile näha" + permission_manage_wiki: "Vikit hallata" + permission_rename_wiki_pages: "Vikilehti ümber nimetada" + permission_delete_wiki_pages: "Vikilehti kustutada" + permission_view_wiki_pages: "Vikit näha" + permission_view_wiki_edits: "Viki ajalugu näha" + permission_edit_wiki_pages: "Vikilehti muuta" + permission_delete_wiki_pages_attachments: "Manuseid kustutada" + permission_protect_wiki_pages: "Vikilehti kaitsta" + permission_manage_repository: "Hoidlaid hallata" + permission_browse_repository: "Hoidlaid sirvida" + permission_view_changesets: "Sissekandeid näha" + permission_commit_access: "Sissekandeid teha" + permission_manage_boards: "Foorumeid hallata" + permission_view_messages: "Postitusi näha" + permission_add_messages: "Postitusi lisada" + permission_edit_messages: "Postitusi muuta" + permission_edit_own_messages: "Omi postitusi muuta" + permission_delete_messages: "Postitusi kustutada" + permission_delete_own_messages: "Omi postitusi kustutada" + permission_export_wiki_pages: "Vikilehti eksportida" + permission_manage_subtasks: "Alamteemasid hallata" + permission_manage_related_issues: "Seotud teemasid hallata" + + project_module_issue_tracking: "Teemade jälgimine" + project_module_time_tracking: "Ajakulu arvestus" + project_module_news: "Uudised" + project_module_documents: "Dokumendid" + project_module_files: "Failid" + project_module_wiki: "Viki" + project_module_repository: "Hoidlad" + project_module_boards: "Foorumid" + project_module_calendar: "Kalender" + project_module_gantt: "Gantt" + + label_user: "Kasutaja" + label_user_plural: "Kasutajad" + label_user_new: "Uus kasutaja" + label_user_anonymous: "Anonüümne" + label_project: "Projekt" + label_project_new: "Uus projekt" + label_project_plural: "Projektid" + label_x_projects: + zero: "pole projekte" + one: "1 projekt" + other: "%{count} projekti" + label_project_all: "Kõik projektid" + label_project_latest: "Viimased projektid" + label_issue: "Teema" + label_issue_new: "Uus teema" + label_issue_plural: "Teemad" + label_issue_view_all: "Teemade nimekiri" + label_issues_by: "Teemad %{value} järgi" + label_issue_added: "Teema lisatud" + label_issue_updated: "Teema uuendatud" + label_issue_note_added: "Märkus lisatud" + label_issue_status_updated: "Olek uuendatud" + label_issue_priority_updated: "Prioriteet uuendatud" + label_document: "Dokument" + label_document_new: "Uus dokument" + label_document_plural: "Dokumendid" + label_document_added: "Dokument lisatud" + label_role: "Roll" + label_role_plural: "Rollid" + label_role_new: "Uus roll" + label_role_and_permissions: "Rollid ja õigused" + label_role_anonymous: "Anonüümne" + label_role_non_member: "Mitteliige" + label_member: "Liige" + label_member_new: "Uus liige" + label_member_plural: "Liikmed" + label_tracker: "Valdkond" + label_tracker_plural: "Valdkonnad" + label_tracker_new: "Uus valdkond" + label_workflow: "Töövood" + label_issue_status: "Olek" + label_issue_status_plural: "Olekud" + label_issue_status_new: "Uus olek" + label_issue_category: "Kategooria" + label_issue_category_plural: "Kategooriad" + label_issue_category_new: "Uus kategooria" + label_custom_field: "Omaloodud väli" + label_custom_field_plural: "Omaloodud väljad" + label_custom_field_new: "Uus väli" + label_enumerations: "Loetelud" + label_enumeration_new: "Uus väärtus" + label_information: "Teave" + label_information_plural: "Teave" + label_please_login: "Palun logi sisse" + label_register: "Registreeru" + label_login_with_open_id_option: "või logi sisse OpenID-ga" + label_password_lost: "Kui parool on ununud..." + label_home: "Kodu" + label_my_page: "Oma leht" + label_my_account: "Oma konto" + label_my_projects: "Oma projektid" + label_my_page_block: "Uus blokk" + label_administration: "Seadistused" + label_login: "Logi sisse" + label_logout: "Logi välja" + label_help: "Abi" + label_reported_issues: "Minu poolt lisatud teemad" + label_assigned_to_me_issues: "Minu teha olevad teemad" + label_last_login: "Viimane ühendus" + label_registered_on: "Registreeritud" + label_activity: "Ajalugu" + label_overall_activity: "Üldine tegevuste ajalugu" + label_user_activity: "%{value} tegevuste ajalugu" + label_new: "Uus" + label_logged_as: "Sisse logitud kui" + label_environment: "Keskkond" + label_authentication: "Autentimine" + label_auth_source: "Autentimisallikas" + label_auth_source_new: "Uus autentimisallikas" + label_auth_source_plural: "Autentimisallikad" + label_subproject_plural: "Alamprojektid" + label_subproject_new: "Uus alamprojekt" + label_and_its_subprojects: "%{value} ja selle alamprojektid" + label_min_max_length: "Min.-maks. pikkus" + label_list: "Nimekiri" + label_date: "Kuupäev" + label_integer: "Täisarv" + label_float: "Ujukomaarv" + label_boolean: "Tõeväärtus" + label_string: "Tekst" + label_text: "Pikk tekst" + label_attribute: "Atribuut" + label_attribute_plural: "Atribuudid" + label_no_data: "Pole" + label_change_status: "Muuda olekut" + label_history: "Ajalugu" + label_attachment: "Fail" + label_attachment_new: "Uus fail" + label_attachment_delete: "Kustuta fail" + label_attachment_plural: "Failid" + label_file_added: "Fail lisatud" + label_report: "Aruanne" + label_report_plural: "Aruanded" + label_news: "Uudised" + label_news_new: "Lisa uudis" + label_news_plural: "Uudised" + label_news_latest: "Viimased uudised" + label_news_view_all: "Kõik uudised" + label_news_added: "Uudis lisatud" + label_news_comment_added: "Kommentaar uudisele lisatud" + label_settings: "Seaded" + label_overview: "Ülevaade" + label_version: "Versioon" + label_version_new: "Uus versioon" + label_version_plural: "Versioonid" + label_close_versions: "Sulge lõpetatud versioonid" + label_confirmation: "Kinnitus" + label_export_to: "Samuti saadaval kujul:" + label_read: "Loe..." + label_public_projects: "Avalikud projektid" + label_open_issues: "avatud" + label_open_issues_plural: "avatud" + label_closed_issues: "suletud" + label_closed_issues_plural: "suletud" + label_x_open_issues_abbr_on_total: + zero: "0 avatud / %{total}" + one: "1 avatud / %{total}" + other: "%{count} avatud / %{total}" + label_x_open_issues_abbr: + zero: "0 avatud" + one: "1 avatud" + other: "%{count} avatud" + label_x_closed_issues_abbr: + zero: "0 suletud" + one: "1 suletud" + other: "%{count} suletud" + label_x_issues: + zero: "0 teemat" + one: "1 teema" + other: "%{count} teemat" + label_total: "Kokku" + label_permissions: "Õigused" + label_current_status: "Praegune olek" + label_new_statuses_allowed: "Uued lubatud olekud" + label_all: "kõik" + label_none: "pole" + label_nobody: "eikeegi" + label_next: "Järgmine" + label_previous: "Eelmine" + label_used_by: "Kasutab" + label_details: "Üksikasjad" + label_add_note: "Lisa märkus" + label_per_page: "Lehe kohta" + label_calendar: "Kalender" + label_months_from: "kuu kaugusel" + label_gantt: "Gantt" + label_internal: "Sisemine" + label_last_changes: "viimased %{count} muudatust" + label_change_view_all: "Kõik muudatused" + label_personalize_page: "Kujunda leht ümber" + label_comment: "Kommentaar" + label_comment_plural: "Kommentaarid" + label_x_comments: + zero: "kommentaare pole" + one: "1 kommentaar" + other: "%{count} kommentaari" + label_comment_add: "Lisa kommentaar" + label_comment_added: "Kommentaar lisatud" + label_comment_delete: "Kustuta kommentaar" + label_query: "Omaloodud päring" + label_query_plural: "Omaloodud päringud" + label_query_new: "Uus päring" + label_my_queries: "Mu omaloodud päringud" + label_filter_add: "Lisa filter" + label_filter_plural: "Filtrid" + label_equals: "on" + label_not_equals: "ei ole" + label_in_less_than: "on väiksem kui" + label_in_more_than: "on suurem kui" + label_greater_or_equal: "suurem-võrdne" + label_less_or_equal: "väiksem-võrdne" + label_between: "vahemikus" + label_in: "sisaldub hulgas" + label_today: "täna" + label_all_time: "piirideta" + label_yesterday: "eile" + label_this_week: "sel nädalal" + label_last_week: "eelmisel nädalal" + label_last_n_days: "viimase %{count} päeva jooksul" + label_this_month: "sel kuul" + label_last_month: "eelmisel kuul" + label_this_year: "sel aastal" + label_date_range: "Kuupäevavahemik" + label_less_than_ago: "uuem kui" + label_more_than_ago: "vanem kui" + label_ago: "vanus" + label_contains: "sisaldab" + label_not_contains: "ei sisalda" + label_day_plural: "päeva" + label_repository: "Hoidla" + label_repository_new: "Uus hoidla" + label_repository_plural: "Hoidlad" + label_browse: "Sirvi" + label_branch: "Haru" + label_tag: "Sildiga" + label_revision: "Sissekanne" + label_revision_plural: "Sissekanded" + label_revision_id: "Sissekande kood %{value}" + label_associated_revisions: "Seotud sissekanded" + label_added: "lisatud" + label_modified: "muudetud" + label_copied: "kopeeritud" + label_renamed: "ümber nimetatud" + label_deleted: "kustutatud" + label_latest_revision: "Viimane sissekanne" + label_latest_revision_plural: "Viimased sissekanded" + label_view_revisions: "Haru ajalugu" + label_view_all_revisions: "Kogu ajalugu" + label_max_size: "Suurim pikkus" + label_sort_highest: "Nihuta esimeseks" + label_sort_higher: "Nihuta üles" + label_sort_lower: "Nihuta alla" + label_sort_lowest: "Nihuta viimaseks" + label_roadmap: "Teekaart" + label_roadmap_due_in: "Tähtaeg %{value}" + label_roadmap_overdue: "%{value} hiljaks jäänud" + label_roadmap_no_issues: "Selles versioonis ei ole teemasid" + label_search: "Otsi" + label_result_plural: "Tulemused" + label_all_words: "Kõik sõnad" + label_wiki: "Viki" + label_wiki_edit: "Viki muutmine" + label_wiki_edit_plural: "Viki muutmised" + label_wiki_page: "Vikileht" + label_wiki_page_plural: "Vikilehed" + label_index_by_title: "Järjesta pealkirja järgi" + label_index_by_date: "Järjesta kuupäeva järgi" + label_current_version: "Praegune versioon" + label_preview: "Eelvaade" + label_feed_plural: "Vood" + label_changes_details: "Kõigi muudatuste üksikasjad" + label_issue_tracking: "Teemade jälgimine" + label_spent_time: "Kulutatud aeg" + label_overall_spent_time: "Kokku kulutatud aeg" + label_f_hour: "%{value} tund" + label_f_hour_plural: "%{value} tundi" + label_time_tracking: "Ajakulu arvestus" + label_change_plural: "Muudatused" + label_statistics: "Statistika" + label_commits_per_month: "Sissekandeid kuu kohta" + label_commits_per_author: "Sissekandeid autori kohta" + label_diff: "erinevused" + label_view_diff: "Vaata erinevusi" + label_diff_inline: "teksti sees" + label_diff_side_by_side: "kõrvuti" + label_options: "Valikud" + label_copy_workflow_from: "Kopeeri see töövoog" + label_permissions_report: "Õiguste aruanne" + label_watched_issues: "Jälgitud teemad" + label_related_issues: "Seotud teemad" + label_applied_status: "Kehtestatud olek" + label_loading: "Laadimas..." + label_relation_new: "Uus seos" + label_relation_delete: "Kustuta seos" + label_relates_to: "seostub" + label_duplicates: "duplitseerib" + label_duplicated_by: "duplitseerija" + label_blocks: "blokeerib" + label_blocked_by: "blokeerija" + label_precedes: "eelneb" + label_follows: "järgneb" + label_end_to_start: "lõpust alguseni" + label_end_to_end: "lõpust lõpuni" + label_start_to_start: "algusest alguseni" + label_start_to_end: "algusest lõpuni" + label_stay_logged_in: "Püsi sisselogituna" + label_disabled: "pole võimalik" + label_show_completed_versions: "Näita lõpetatud versioone" + label_me: "mina" + label_board: "Foorum" + label_board_new: "Uus foorum" + label_board_plural: "Foorumid" + label_board_locked: "Lukus" + label_board_sticky: "Püsiteema" + label_topic_plural: "Teemad" + label_message_plural: "Postitused" + label_message_last: "Viimane postitus" + label_message_new: "Uus postitus" + label_message_posted: "Postitus lisatud" + label_reply_plural: "Vastused" + label_send_information: "Saada teave konto kasutajale" + label_year: "Aasta" + label_month: "Kuu" + label_week: "Nädal" + label_date_from: "Alates" + label_date_to: "Kuni" + label_language_based: "Kasutaja keele põhjal" + label_sort_by: "Sorteeri %{value} järgi" + label_send_test_email: "Saada kontrollkiri" + label_feeds_access_key: "Atom juurdepääsuvõti" + label_missing_feeds_access_key: "Atom juurdepääsuvõti on puudu" + label_feeds_access_key_created_on: "Atom juurdepääsuvõti loodi %{value} tagasi" + label_module_plural: "Moodulid" + label_added_time_by: "Lisatud %{author} poolt %{age} tagasi" + label_updated_time_by: "Uuendatud %{author} poolt %{age} tagasi" + label_updated_time: "Uuendatud %{value} tagasi" + label_jump_to_a_project: "Ava projekt..." + label_file_plural: "Failid" + label_changeset_plural: "Muudatused" + label_default_columns: "Vaikimisi veerud" + label_no_change_option: "(Ei muutu)" + label_bulk_edit_selected_issues: "Muuda valitud teemasid korraga" + label_bulk_edit_selected_time_entries: "Muuda valitud ajakandeid korraga" + label_theme: "Visuaalne teema" + label_default: "Tavaline" + label_search_titles_only: "Ainult pealkirjadest" + label_user_mail_option_all: "Kõigist tegevustest kõigis mu projektides" + label_user_mail_option_selected: "Kõigist tegevustest ainult valitud projektides..." + label_user_mail_option_none: "Teavitusi ei saadeta" + label_user_mail_option_only_my_events: "Ainult mu jälgitavatest või minuga seotud tegevustest" + label_user_mail_option_only_assigned: "Ainult minu teha olevate asjade kohta" + label_user_mail_option_only_owner: "Ainult mu oma asjade kohta" + label_user_mail_no_self_notified: "Ära teavita mind mu enda tehtud muudatustest" + label_registration_activation_by_email: "e-kirjaga" + label_registration_manual_activation: "käsitsi" + label_registration_automatic_activation: "automaatselt" + label_display_per_page: "Lehe kohta: %{value}" + label_age: "Vanus" + label_change_properties: "Muuda omadusi" + label_general: "Üldine" + label_more: "Rohkem" + label_scm: "Lähtekoodi haldusvahend" + label_plugins: "Lisamoodulid" + label_ldap_authentication: "LDAP autentimine" + label_downloads_abbr: "A/L" + label_optional_description: "Teave" + label_add_another_file: "Lisa veel üks fail" + label_preferences: "Eelistused" + label_chronological_order: "kronoloogiline" + label_reverse_chronological_order: "tagurpidi kronoloogiline" + label_planning: "Planeerimine" + label_incoming_emails: "Sissetulevad e-kirjad" + label_generate_key: "Genereeri võti" + label_issue_watchers: "Jälgijad" + label_example: "Näide" + label_display: "Kujundus" + label_sort: "Sorteeri" + label_ascending: "Kasvavalt" + label_descending: "Kahanevalt" + label_date_from_to: "Alates %{start} kuni %{end}" + label_wiki_content_added: "Vikileht lisatud" + label_wiki_content_updated: "Vikileht uuendatud" + label_group: "Grupp" + label_group_plural: "Grupid" + label_group_new: "Uus grupp" + label_time_entry_plural: "Kulutatud aeg" + label_version_sharing_none: "ei toimu" + label_version_sharing_descendants: "alamprojektidega" + label_version_sharing_hierarchy: "projektihierarhiaga" + label_version_sharing_tree: "projektipuuga" + label_version_sharing_system: "kõigi projektidega" + label_update_issue_done_ratios: "Uuenda edenemise astmeid" + label_copy_source: "Allikas" + label_copy_target: "Sihtkoht" + label_copy_same_as_target: "Sama mis sihtkoht" + label_display_used_statuses_only: "Näita ainult selles valdkonnas kasutusel olekuid" + label_api_access_key: "API juurdepääsuvõti" + label_missing_api_access_key: "API juurdepääsuvõti on puudu" + label_api_access_key_created_on: "API juurdepääsuvõti loodi %{value} tagasi" + label_profile: "Profiil" + label_subtask_plural: "Alamteemad" + label_project_copy_notifications: "Saada projekti kopeerimise kohta teavituskiri" + label_principal_search: "Otsi kasutajat või gruppi:" + label_user_search: "Otsi kasutajat:" + label_additional_workflow_transitions_for_author: "Luba ka järgmisi üleminekuid kui kasutaja on teema looja" + label_additional_workflow_transitions_for_assignee: "Luba ka järgmisi üleminekuid kui kasutaja on teemaga tegeleja" + label_issues_visibility_all: "kõiki teemasid" + label_issues_visibility_public: "kõiki mitteprivaatseid teemasid" + label_issues_visibility_own: "enda poolt loodud või enda teha teemasid" + label_git_report_last_commit: "Viimase sissekande teave otse failinimekirja" + label_parent_revision: "Eellane" + label_child_revision: "Järglane" + label_export_options: "%{export_format} ekspordivalikud" + label_copy_attachments: "Kopeeri manused" + label_item_position: "%{position}/%{count}" + label_completed_versions: "Lõpetatud versioonid" + label_search_for_watchers: "Otsi lisamiseks jälgijaid" + + button_login: "Logi sisse" + button_submit: "Sisesta" + button_save: "Salvesta" + button_check_all: "Märgi kõik" + button_uncheck_all: "Nulli valik" + button_collapse_all: "Voldi kõik kokku" + button_expand_all: "Voldi kõik lahti" + button_delete: "Kustuta" + button_create: "Loo" + button_create_and_continue: "Loo ja jätka" + button_test: "Testi" + button_edit: "Muuda" + button_edit_associated_wikipage: "Muuda seotud vikilehte: %{page_title}" + button_add: "Lisa" + button_change: "Muuda" + button_apply: "Lae" + button_clear: "Puhasta" + button_lock: "Lukusta" + button_unlock: "Ava lukust" + button_download: "Lae alla" + button_list: "Listi" + button_view: "Vaata" + button_move: "Tõsta" + button_move_and_follow: "Tõsta ja järgne" + button_back: "Tagasi" + button_cancel: "Katkesta" + button_activate: "Aktiveeri" + button_sort: "Sorteeri" + button_log_time: "Ajakulu" + button_rollback: "Rulli tagasi sellesse versiooni" + button_watch: "Jälgi" + button_unwatch: "Ära jälgi" + button_reply: "Vasta" + button_archive: "Arhiveeri" + button_unarchive: "Arhiivist tagasi" + button_reset: "Nulli" + button_rename: "Nimeta ümber" + button_change_password: "Vaheta parool" + button_copy: "Kopeeri" + button_copy_and_follow: "Kopeeri ja järgne" + button_annotate: "Annoteeri" + button_update: "Muuda" + button_configure: "Konfigureeri" + button_quote: "Tsiteeri" + button_duplicate: "Duplitseeri" + button_show: "Näita" + button_edit_section: "Muuda seda sektsiooni" + button_export: "Ekspordi" + button_delete_my_account: "Kustuta oma konto" + + status_active: "aktiivne" + status_registered: "registreeritud" + status_locked: "lukus" + + version_status_open: "avatud" + version_status_locked: "lukus" + version_status_closed: "suletud" + + field_active: "Aktiivne" + + text_select_mail_notifications: "Tegevused, millest peaks e-kirjaga teavitama" + text_regexp_info: "nt. ^[A-Z0-9]+$" + text_min_max_length_info: "0 tähendab, et piiranguid ei ole" + text_project_destroy_confirmation: "Oled Sa kindel oma soovis see projekt täielikult kustutada?" + text_subprojects_destroy_warning: "Alamprojekt(id) - %{value} - kustutatakse samuti." + text_workflow_edit: "Töövoo muutmiseks vali roll ja valdkond" + text_are_you_sure: "Oled Sa kindel?" + text_journal_changed: "%{label} muudetud %{old} -> %{new}" + text_journal_changed_no_detail: "%{label} uuendatud" + text_journal_set_to: "%{label} uus väärtus on %{value}" + text_journal_deleted: "%{label} kustutatud (%{old})" + text_journal_added: "%{label} %{value} lisatud" + text_tip_issue_begin_day: "teema avamise päev" + text_tip_issue_end_day: "teema sulgemise päev" + text_tip_issue_begin_end_day: "teema avati ja sulgeti samal päeval" + text_project_identifier_info: "Lubatud on ainult väikesed tähed (a-z), numbrid ja kriipsud.
    Peale salvestamist ei saa tunnust enam muuta." + text_caracters_maximum: "%{count} märki kõige rohkem." + text_caracters_minimum: "Peab olema vähemalt %{count} märki pikk." + text_length_between: "Pikkus %{min} kuni %{max} märki." + text_tracker_no_workflow: "Selle valdkonna jaoks ei ole ühtegi töövoogu kirjeldatud" + text_unallowed_characters: "Lubamatud märgid" + text_comma_separated: "Lubatud erinevad väärtused (komaga eraldatult)." + text_line_separated: "Lubatud erinevad väärtused (igaüks eraldi real)." + text_issues_ref_in_commit_messages: "Teemadele ja parandustele viitamine sissekannete märkustes" + text_issue_added: "%{author} lisas uue teema %{id}." + text_issue_updated: "%{author} uuendas teemat %{id}." + text_wiki_destroy_confirmation: "Oled Sa kindel oma soovis kustutada see Viki koos kogu sisuga?" + text_issue_category_destroy_question: "Kustutatavat kategooriat kasutab %{count} teema(t). Mis Sa soovid nendega ette võtta?" + text_issue_category_destroy_assignments: "Jäta teemadel kategooria määramata" + text_issue_category_reassign_to: "Määra teemad teise kategooriasse" + text_user_mail_option: "Valimata projektidest saad teavitusi ainult jälgitavate või Sinuga seotud asjade kohta (nt. Sinu loodud või teha teemad)." + text_no_configuration_data: "Rollid, valdkonnad, olekud ja töövood ei ole veel seadistatud.\nVäga soovitav on laadida vaikeasetused. Peale laadimist saad neid ise muuta." + text_load_default_configuration: "Lae vaikeasetused" + text_status_changed_by_changeset: "Kehtestatakse muudatuses %{value}." + text_time_logged_by_changeset: "Kehtestatakse muudatuses %{value}." + text_issues_destroy_confirmation: "Oled Sa kindel oma soovis valitud teema(d) kustutada?" + text_issues_destroy_descendants_confirmation: "See kustutab samuti %{count} alamteemat." + text_time_entries_destroy_confirmation: "Oled Sa kindel oma soovis valitud ajakulu kanne/kanded kustutada?" + text_select_project_modules: "Projektis kasutatavad moodulid" + text_default_administrator_account_changed: "Algne administraatori konto on muudetud" + text_file_repository_writable: "Manuste kataloog on kirjutatav" + text_plugin_assets_writable: "Lisamoodulite abifailide kataloog on kirjutatav" + text_rmagick_available: "RMagick on kasutatav (mittekohustuslik)" + text_destroy_time_entries_question: "Kustutatavatele teemadele oli kirja pandud %{hours} tundi. Mis Sa soovid ette võtta?" + text_destroy_time_entries: "Kustuta need tunnid" + text_assign_time_entries_to_project: "Vii tunnid üle teise projekti" + text_reassign_time_entries: "Määra tunnid sellele teemale:" + text_user_wrote: "%{value} kirjutas:" + text_enumeration_destroy_question: "Selle väärtusega on seotud %{count} objekt(i)." + text_enumeration_category_reassign_to: "Seo nad teise väärtuse külge:" + text_email_delivery_not_configured: "E-kirjade saatmine ei ole seadistatud ja teavitusi ei saadeta.\nKonfigureeri oma SMTP server failis config/configuration.yml ja taaskäivita Redmine." + text_repository_usernames_mapping: "Seosta Redmine kasutaja hoidlasse sissekannete tegijaga.\nSama nime või e-postiga kasutajad seostatakse automaatselt." + text_diff_truncated: "... Osa erinevusi jäi välja, sest neid on näitamiseks liiga palju." + text_custom_field_possible_values_info: "Üks rida iga väärtuse jaoks" + text_wiki_page_destroy_question: "Sel lehel on %{descendants} järglasleht(e) ja järeltulija(t). Mis Sa soovid ette võtta?" + text_wiki_page_nullify_children: "Muuda järglaslehed uuteks juurlehtedeks" + text_wiki_page_destroy_children: "Kustuta järglaslehed ja kõik nende järglased" + text_wiki_page_reassign_children: "Määra järglaslehed teise lehe külge" + text_own_membership_delete_confirmation: "Sa võtad endalt ära osa või kõik õigused ega saa edaspidi seda projekti võib-olla enam muuta.\nOled Sa jätkamises kindel?" + text_zoom_in: "Vaata lähemalt" + text_zoom_out: "Vaata kaugemalt" + text_warn_on_leaving_unsaved: "Sel lehel on salvestamata teksti, mis läheb kaduma, kui siit lehelt lahkud." + text_scm_path_encoding_note: "Vaikimisi UTF-8" + text_git_repository_note: "Hoidla peab olema paljas (bare) ja kohalik (nt. /gitrepo, c:\\gitrepo)" + text_mercurial_repository_note: "Hoidla peab olema kohalik (nt. /hgrepo, c:\\hgrepo)" + text_scm_command: "Hoidla poole pöördumise käsk" + text_scm_command_version: "Versioon" + text_scm_config: "Hoidlate poole pöördumist saab konfigureerida failis config/configuration.yml. Peale selle muutmist taaskäivita Redmine." + text_scm_command_not_available: "Hoidla poole pöördumine ebaõnnestus. Palun kontrolli seadistusi." + text_issue_conflict_resolution_overwrite: "Kehtesta oma muudatused (kõik märkused jäävad, ent muu võidakse üle kirjutada)" + text_issue_conflict_resolution_add_notes: "Lisa oma märkused, aga loobu teistest muudatustest" + text_issue_conflict_resolution_cancel: "Loobu kõigist muudatustest ja lae %{link} uuesti" + text_account_destroy_confirmation: "Oled Sa kindel?\nSu konto kustutatakse jäädavalt ja seda pole võimalik taastada." + + default_role_manager: "Haldaja" + default_role_developer: "Arendaja" + default_role_reporter: "Edastaja" + default_tracker_bug: "Veaparandus" + default_tracker_feature: "Täiendus" + default_tracker_support: "Klienditugi" + default_issue_status_new: "Avatud" + default_issue_status_in_progress: "Töös" + default_issue_status_resolved: "Lahendatud" + default_issue_status_feedback: "Tagasiside" + default_issue_status_closed: "Suletud" + default_issue_status_rejected: "Tagasi lükatud" + default_doc_category_user: "Juhend lõppkasutajale" + default_doc_category_tech: "Tehniline dokumentatsioon" + default_priority_low: "Aega on" + default_priority_normal: "Tavaline" + default_priority_high: "Pakiline" + default_priority_urgent: "Täna vaja" + default_priority_immediate: "Kohe vaja" + default_activity_design: "Kavandamine" + default_activity_development: "Arendamine" + + enumeration_issue_priorities: "Teemade prioriteedid" + enumeration_doc_categories: "Dokumentide kategooriad" + enumeration_activities: "Tegevused (ajakulu)" + enumeration_system_activity: "Süsteemi aktiivsus" + description_filter: "Filter" + description_search: "Otsinguväli" + description_choose_project: "Projektid" + description_project_scope: "Otsingu ulatus" + description_notes: "Märkused" + description_message_content: "Postituse sisu" + description_query_sort_criteria_attribute: "Sorteerimise kriteerium" + description_query_sort_criteria_direction: "Sorteerimise suund" + description_user_mail_notification: "E-kirjaga teavitamise seaded" + description_available_columns: "Kasutatavad veerud" + description_selected_columns: "Valitud veerud" + description_all_columns: "Kõik veerud" + description_issue_category_reassign: "Vali uus kategooria" + description_wiki_subpages_reassign: "Vali lehele uus vanem" + description_date_range_list: "Vali vahemik nimekirjast" + description_date_range_interval: "Vali vahemik algus- ja lõpukuupäeva abil" + description_date_from: "Sisesta alguskuupäev" + description_date_to: "Sisesta lõpukuupäev" + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: "Lubatud on ainult väikesed tähed (a-z), numbrid ja kriipsud.
    Peale salvestamist ei saa tunnust enam muuta." + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: "kõik" + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: "alamprojektidega" + label_cross_project_tree: "projektipuuga" + label_cross_project_hierarchy: "projektihierarhiaga" + label_cross_project_system: "kõigi projektidega" + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past + label_attribute_of_user: User's %{name} + text_turning_multiple_off: If you disable multiple values, multiple values will be + removed in order to preserve only one value per item. + label_attribute_of_issue: Issue's %{name} + permission_add_documents: Add documents + permission_edit_documents: Edit documents + permission_delete_documents: Delete documents + label_gantt_progress_line: Progress line + setting_jsonp_enabled: Enable JSONP support + field_inherit_members: Inherit members + field_closed_on: Closed + field_generate_password: Generate password + setting_default_projects_tracker_ids: Default trackers for new projects + label_total_time: "Kokku" + notice_account_not_activated_yet: You haven't activated your account yet. If you want + to receive a new activation email, please click this link. + notice_account_locked: Your account is locked. + label_hidden: Hidden + label_visibility_private: to me only + label_visibility_roles: to these roles only + label_visibility_public: to any users + field_must_change_passwd: Must change password at next logon + notice_new_password_must_be_different: The new password must be different from the + current password + setting_mail_handler_excluded_filenames: Exclude attachments by name + text_convert_available: ImageMagick convert available (optional) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/293d0a71c3ab0c1ab7c165e236d1eff8e57cb305.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/293d0a71c3ab0c1ab7c165e236d1eff8e57cb305.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,122 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueCategoriesController < ApplicationController + menu_item :settings + model_object IssueCategory + before_filter :find_model_object, :except => [:index, :new, :create] + before_filter :find_project_from_association, :except => [:index, :new, :create] + before_filter :find_project_by_project_id, :only => [:index, :new, :create] + before_filter :authorize + accept_api_auth :index, :show, :create, :update, :destroy + + def index + respond_to do |format| + format.html { redirect_to_settings_in_projects } + format.api { @categories = @project.issue_categories.all } + end + end + + def show + respond_to do |format| + format.html { redirect_to_settings_in_projects } + format.api + end + end + + def new + @category = @project.issue_categories.build + @category.safe_attributes = params[:issue_category] + + respond_to do |format| + format.html + format.js + end + end + + def create + @category = @project.issue_categories.build + @category.safe_attributes = params[:issue_category] + if @category.save + respond_to do |format| + format.html do + flash[:notice] = l(:notice_successful_create) + redirect_to_settings_in_projects + end + format.js + format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) } + end + else + respond_to do |format| + format.html { render :action => 'new'} + format.js { render :action => 'new'} + format.api { render_validation_errors(@category) } + end + end + end + + def edit + end + + def update + @category.safe_attributes = params[:issue_category] + if @category.save + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to_settings_in_projects + } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@category) } + end + end + end + + def destroy + @issue_count = @category.issues.size + if @issue_count == 0 || params[:todo] || api_request? + reassign_to = nil + if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?) + reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) + end + @category.destroy(reassign_to) + respond_to do |format| + format.html { redirect_to_settings_in_projects } + format.api { render_api_ok } + end + return + end + @categories = @project.issue_categories - [@category] + end + + private + + def redirect_to_settings_in_projects + redirect_to settings_project_path(@project, :tab => 'categories') + end + + # Wrap ApplicationController's find_model_object method to set + # @category instead of just @issue_category + def find_model_object + super + @category = @object + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/2973e97a2549893414a5beb545b08538df56d208.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/2973e97a2549893414a5beb545b08538df56d208.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,18 @@ +class SetDocAndFilesNotifications < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.where(:controller => "projects", :action => "add_file").each {|p| p.update_attribute(:mail_option, true)} + Permission.where(:controller => "projects", :action => "add_document").each {|p| p.update_attribute(:mail_option, true)} + Permission.where(:controller => "documents", :action => "add_attachment").each {|p| p.update_attribute(:mail_option, true)} + Permission.where(:controller => "issues", :action => "add_attachment").each {|p| p.update_attribute(:mail_option, true)} + end + + def self.down + Permission.where(:controller => "projects", :action => "add_file").each {|p| p.update_attribute(:mail_option, false)} + Permission.where(:controller => "projects", :action => "add_document").each {|p| p.update_attribute(:mail_option, false)} + Permission.where(:controller => "documents", :action => "add_attachment").each {|p| p.update_attribute(:mail_option, false)} + Permission.where(:controller => "issues", :action => "add_attachment").each {|p| p.update_attribute(:mail_option, false)} + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/298be127abf8c78d6ccda6e13729844112a36c53.svn-base --- a/.svn/pristine/29/298be127abf8c78d6ccda6e13729844112a36c53.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,131 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class Journal < ActiveRecord::Base - belongs_to :journalized, :polymorphic => true - # added as a quick fix to allow eager loading of the polymorphic association - # since always associated to an issue, for now - belongs_to :issue, :foreign_key => :journalized_id - - belongs_to :user - has_many :details, :class_name => "JournalDetail", :dependent => :delete_all - attr_accessor :indice - - acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, - :description => :notes, - :author => :user, - :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, - :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} - - acts_as_activity_provider :type => 'issues', - :author_key => :user_id, - :find_options => {:include => [{:issue => :project}, :details, :user], - :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + - " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} - - before_create :split_private_notes - - scope :visible, lambda {|*args| - user = args.shift || User.current - - includes(:issue => :project). - where(Issue.visible_condition(user, *args)). - where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) - } - - def save(*args) - # Do not save an empty journal - (details.empty? && notes.blank?) ? false : super - end - - # Returns the new status if the journal contains a status change, otherwise nil - def new_status - c = details.detect {|detail| detail.prop_key == 'status_id'} - (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil - end - - def new_value_for(prop) - c = details.detect {|detail| detail.prop_key == prop} - c ? c.value : nil - end - - def editable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) - end - - def project - journalized.respond_to?(:project) ? journalized.project : nil - end - - def attachments - journalized.respond_to?(:attachments) ? journalized.attachments : nil - end - - # Returns a string of css classes - def css_classes - s = 'journal' - s << ' has-notes' unless notes.blank? - s << ' has-details' unless details.blank? - s << ' private-notes' if private_notes? - s - end - - def notify? - @notify != false - end - - def notify=(arg) - @notify = arg - end - - def recipients - notified = journalized.notified_users - if private_notes? - notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} - end - notified.map(&:mail) - end - - def watcher_recipients - notified = journalized.notified_watchers - if private_notes? - notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} - end - notified.map(&:mail) - end - - private - - def split_private_notes - if private_notes? - if notes.present? - if details.any? - # Split the journal (notes/changes) so we don't have half-private journals - journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) - journal.details = details - journal.save - self.details = [] - self.created_on = journal.created_on - end - else - # Blank notes should not be private - self.private_notes = false - end - end - true - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/29a1561941007c06e8cd16ad09f34224b899351f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/29a1561941007c06e8cd16ad09f34224b899351f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,56 @@ +<%= render :partial => 'action_menu' %> + +<%= title l(:label_workflow) %> + +
    +
      +
    • <%= link_to l(:label_status_transitions), {:action => 'edit', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %>
    • +
    • <%= link_to l(:label_fields_permissions), {:action => 'permissions', :role_id => @role, :tracker_id => @tracker} %>
    • +
    +
    + +

    <%=l(:text_workflow_edit)%>:

    + +<%= form_tag({}, :method => 'get') do %> +

    + + + + + <%= submit_tag l(:button_edit), :name => nil %> + + <%= hidden_field_tag 'used_statuses_only', '0' %> + + +

    +<% end %> + +<% if @tracker && @role && @statuses.any? %> + <%= form_tag({}, :id => 'workflow_form' ) do %> + <%= hidden_field_tag 'tracker_id', @tracker.id %> + <%= hidden_field_tag 'role_id', @role.id %> + <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only] %> +
    + <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %> + +
    + <%= l(:label_additional_workflow_transitions_for_author) %> +
    + <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %> +
    +
    + <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %> + +
    + <%= l(:label_additional_workflow_transitions_for_assignee) %> +
    + <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %> +
    +
    + <%= javascript_tag "hideFieldset($('#assignee_workflows'))" unless @workflows['assignee'].present? %> +
    + <%= submit_tag l(:button_save) %> + <% end %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/29dc17e6511b552f9bb4332f96e34e10ad2b4a77.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/29dc17e6511b552f9bb4332f96e34e10ad2b4a77.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,36 @@ +<% if @project.boards.any? %> + + + + + + + + +<% Board.board_tree(@project.boards) do |board, level| + next if board.new_record? %> + + + + + + +<% end %> + +
    <%= l(:label_board) %><%= l(:field_description) %>
    <%= link_to board.name, project_board_path(@project, board) %><%=h board.description %> + <% if authorize_for("boards", "edit") %> + <%= reorder_links('board', {:controller => 'boards', :action => 'update', :project_id => @project, :id => board}, :put) %> + <% end %> + + <% if User.current.allowed_to?(:manage_boards, @project) %> + <%= link_to l(:button_edit), edit_project_board_path(@project, board), :class => 'icon icon-edit' %> + <%= delete_link project_board_path(@project, board) %> + <% end %> +
    +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> + +<% if User.current.allowed_to?(:manage_boards, @project) %> +

    <%= link_to l(:label_board_new), new_project_board_path(@project), :class => 'icon icon-add' %>

    +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/29e8e11e7b7fba04b2ca11546151e862eaeb8161.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/29e8e11e7b7fba04b2ca11546151e862eaeb8161.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,554 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 ChangesetTest < ActiveSupport::TestCase + fixtures :projects, :repositories, + :issues, :issue_statuses, :issue_categories, + :changesets, :changes, + :enumerations, + :custom_fields, :custom_values, + :users, :members, :member_roles, :trackers, + :enabled_modules, :roles + + def test_ref_keywords_any + ActionMailer::Base.deliveries.clear + Setting.commit_ref_keywords = '*' + Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}] + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'New commit (#2). Fixes #1', + :revision => '12345') + assert c.save + assert_equal [1, 2], c.issue_ids.sort + fixed = Issue.find(1) + assert fixed.closed? + assert_equal 90, fixed.done_ratio + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_ref_keywords + Setting.commit_ref_keywords = 'refs' + Setting.commit_update_keywords = '' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'Ignores #2. Refs #1', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_any_only + Setting.commit_ref_keywords = '*' + Setting.commit_update_keywords = '' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'Ignores #2. Refs #1', + :revision => '12345') + assert c.save + assert_equal [1, 2], c.issue_ids.sort + end + + def test_ref_keywords_any_with_timelog + Setting.commit_ref_keywords = '*' + Setting.commit_logtime_enabled = '1' + + { + '2' => 2.0, + '2h' => 2.0, + '2hours' => 2.0, + '15m' => 0.25, + '15min' => 0.25, + '3h15' => 3.25, + '3h15m' => 3.25, + '3h15min' => 3.25, + '3:15' => 3.25, + '3.25' => 3.25, + '3.25h' => 3.25, + '3,25' => 3.25, + '3,25h' => 3.25, + }.each do |syntax, expected_hours| + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => 24.hours.ago, + :comments => "Worked on this issue #1 @#{syntax}", + :revision => '520', + :user => User.find(2)) + assert_difference 'TimeEntry.count' do + c.scan_comment_for_issue_ids + end + assert_equal [1], c.issue_ids.sort + + time = TimeEntry.order('id desc').first + assert_equal 1, time.issue_id + assert_equal 1, time.project_id + assert_equal 2, time.user_id + assert_equal expected_hours, time.hours, + "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}" + assert_equal Date.yesterday, time.spent_on + assert time.activity.is_default? + assert time.comments.include?('r520'), + "r520 was expected in time_entry comments: #{time.comments}" + end + end + + def test_ref_keywords_closing_with_timelog + Setting.commit_ref_keywords = '*' + Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', + 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}] + Setting.commit_logtime_enabled = '1' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'This is a comment. Fixes #1 @4.5, #2 @1', + :user => User.find(2)) + assert_difference 'TimeEntry.count', 2 do + c.scan_comment_for_issue_ids + end + + assert_equal [1, 2], c.issue_ids.sort + assert Issue.find(1).closed? + assert Issue.find(2).closed? + + times = TimeEntry.order('id desc').limit(2) + assert_equal [1, 2], times.collect(&:issue_id).sort + end + + def test_ref_keywords_any_line_start + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '#1 is the reason of this commit', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_allow_brackets_around_a_issue_number + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '[#1] Worked on this issue', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_allow_brackets_around_multiple_issue_numbers + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '[#1 #2, #3] Worked on these', + :revision => '12345') + assert c.save + assert_equal [1,2,3], c.issue_ids.sort + end + + def test_update_keywords_with_multiple_rules + with_settings :commit_update_keywords => [ + {'keywords' => 'fixes, closes', 'status_id' => '5'}, + {'keywords' => 'resolves', 'status_id' => '3'} + ] do + + issue1 = Issue.generate! + issue2 = Issue.generate! + Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}") + assert_equal 5, issue1.reload.status_id + assert_equal 3, issue2.reload.status_id + end + end + + def test_update_keywords_with_multiple_rules_should_match_tracker + with_settings :commit_update_keywords => [ + {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'}, + {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''} + ] do + + issue1 = Issue.generate!(:tracker_id => 2) + issue2 = Issue.generate! + Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}") + assert_equal 5, issue1.reload.status_id + assert_equal 3, issue2.reload.status_id + end + end + + def test_update_keywords_with_multiple_rules_and_no_match + with_settings :commit_update_keywords => [ + {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'}, + {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'} + ] do + + issue1 = Issue.generate!(:tracker_id => 2) + issue2 = Issue.generate! + Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}") + assert_equal 5, issue1.reload.status_id + assert_equal 1, issue2.reload.status_id # no updates + end + end + + def test_commit_referencing_a_subproject_issue + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'refs #5, a subproject issue', + :revision => '12345') + assert c.save + assert_equal [5], c.issue_ids.sort + assert c.issues.first.project != c.project + end + + def test_commit_closing_a_subproject_issue + with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}], + :default_language => 'en' do + issue = Issue.find(5) + assert !issue.closed? + assert_difference 'Journal.count' do + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'closes #5, a subproject issue', + :revision => '12345') + assert c.save + end + assert issue.reload.closed? + journal = Journal.order('id DESC').first + assert_equal issue, journal.issue + assert_include "Applied in changeset ecookbook:r12345.", journal.notes + end + end + + def test_commit_referencing_a_parent_project_issue + # repository of child project + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #2, an issue of a parent project', + :revision => '12345') + assert c.save + assert_equal [2], c.issue_ids.sort + assert c.issues.first.project != c.project + end + + def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + with_settings :commit_cross_project_ref => '0' do + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #4, an issue of a different project', + :revision => '12345') + assert c.save + assert_equal [], c.issue_ids + end + end + + def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + with_settings :commit_cross_project_ref => '1' do + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #4, an issue of a different project', + :revision => '12345') + assert c.save + assert_equal [4], c.issue_ids + end + end + + def test_old_commits_should_not_update_issues_nor_log_time + Setting.commit_ref_keywords = '*' + Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}} + Setting.commit_logtime_enabled = '1' + + repository = Project.find(1).repository + repository.created_on = Time.now + repository.save! + + c = Changeset.new(:repository => repository, + :committed_on => 1.month.ago, + :comments => 'New commit (#2). Fixes #1 @1h', + :revision => '12345') + assert_no_difference 'TimeEntry.count' do + assert c.save + end + assert_equal [1, 2], c.issue_ids.sort + issue = Issue.find(1) + assert_equal 1, issue.status_id + assert_equal 0, issue.done_ratio + end + + def test_text_tag_revision + c = Changeset.new(:revision => '520') + assert_equal 'r520', c.text_tag + end + + def test_text_tag_revision_with_same_project + c = Changeset.new(:revision => '520', :repository => Project.find(1).repository) + assert_equal 'r520', c.text_tag(Project.find(1)) + end + + def test_text_tag_revision_with_different_project + c = Changeset.new(:revision => '520', :repository => Project.find(1).repository) + assert_equal 'ecookbook:r520', c.text_tag(Project.find(2)) + end + + def test_text_tag_revision_with_repository_identifier + r = Repository::Subversion.create!( + :project_id => 1, + :url => 'svn://localhost/test', + :identifier => 'documents') + c = Changeset.new(:revision => '520', :repository => r) + assert_equal 'documents|r520', c.text_tag + assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2)) + end + + def test_text_tag_hash + c = Changeset.new( + :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518', + :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518') + assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag + end + + def test_text_tag_hash_with_same_project + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository) + assert_equal 'commit:7234cb27', c.text_tag(Project.find(1)) + end + + def test_text_tag_hash_with_different_project + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository) + assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2)) + end + + def test_text_tag_hash_all_number + c = Changeset.new(:scmid => '0123456789', :revision => '0123456789') + assert_equal 'commit:0123456789', c.text_tag + end + + def test_text_tag_hash_with_repository_identifier + r = Repository::Subversion.new( + :project_id => 1, + :url => 'svn://localhost/test', + :identifier => 'documents') + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => r) + assert_equal 'commit:documents|7234cb27', c.text_tag + assert_equal 'ecookbook:commit:documents|7234cb27', c.text_tag(Project.find(2)) + end + + def test_previous + changeset = Changeset.find_by_revision('3') + assert_equal Changeset.find_by_revision('2'), changeset.previous + end + + def test_previous_nil + changeset = Changeset.find_by_revision('1') + assert_nil changeset.previous + end + + def test_next + changeset = Changeset.find_by_revision('2') + assert_equal Changeset.find_by_revision('3'), changeset.next + end + + def test_next_nil + changeset = Changeset.find_by_revision('10') + assert_nil changeset.next + end + + def test_comments_should_be_converted_to_utf8 + proj = Project.find(3) + # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") + str = "Texte encod\xe9 en ISO-8859-1." + str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str) + assert( c.save ) + str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1." + str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding) + assert_equal str_utf8, c.comments + end + + def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1 + proj = Project.find(3) + # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") + str1 = "Texte encod\xe9 en ISO-8859-1." + str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test" + str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding) + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'UTF-8' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str1, + :committer => str2) + assert( c.save ) + assert_equal "Texte encod? en ISO-8859-1.", c.comments + assert_equal "?a?b?c?d?e test", c.committer + end + + def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis + proj = Project.find(3) + str = "test\xb5\xfetest\xb5\xfe" + if str.respond_to?(:force_encoding) + str.force_encoding('ASCII-8BIT') + end + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-2022-JP' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str) + assert( c.save ) + assert_equal "test??test??", c.comments + end + + def test_comments_should_be_converted_all_latin1_to_utf8 + s1 = "\xC2\x80" + s2 = "\xc3\x82\xc2\x80" + s4 = s2.dup + if s1.respond_to?(:force_encoding) + s3 = s1.dup + s1.force_encoding('ASCII-8BIT') + s2.force_encoding('ASCII-8BIT') + s3.force_encoding('ISO-8859-1') + s4.force_encoding('UTF-8') + assert_equal s3.encode('UTF-8'), s4 + end + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => s1) + assert( c.save ) + assert_equal s4, c.comments + end + + def test_invalid_utf8_sequences_in_paths_should_be_replaced + proj = Project.find(3) + str1 = "Texte encod\xe9 en ISO-8859-1" + str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test" + str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding) + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'UTF-8' ) + assert r + cs = Changeset.new( + :repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "test") + assert(cs.save) + ch = Change.new( + :changeset => cs, + :action => "A", + :path => str1, + :from_path => str2, + :from_revision => "345") + assert(ch.save) + assert_equal "Texte encod? en ISO-8859-1", ch.path + assert_equal "?a?b?c?d?e test", ch.from_path + end + + def test_comments_nil + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => nil, + :committer => nil) + assert( c.save ) + assert_equal "", c.comments + assert_equal nil, c.committer + if c.comments.respond_to?(:force_encoding) + assert_equal "UTF-8", c.comments.encoding.to_s + end + end + + def test_comments_empty + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "", + :committer => "") + assert( c.save ) + assert_equal "", c.comments + assert_equal "", c.committer + if c.comments.respond_to?(:force_encoding) + assert_equal "UTF-8", c.comments.encoding.to_s + assert_equal "UTF-8", c.committer.encoding.to_s + end + end + + def test_comments_should_accept_more_than_64k + c = Changeset.new(:repository => Repository.first, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "a" * 500.kilobyte) + assert c.save + c.reload + assert_equal 500.kilobyte, c.comments.size + end + + def test_identifier + c = Changeset.find_by_revision('1') + assert_equal c.revision, c.identifier + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/29/29fe660e228b4c672e9942d71f9138fc1988a1fe.svn-base --- a/.svn/pristine/29/29fe660e228b4c672e9942d71f9138fc1988a1fe.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -

    <%= link_to l(:label_group_plural), groups_path %> » <%=h @group %>

    - -
      -<% @group.users.each do |user| %> -
    • <%=h user %>
    • -<% end %> -
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2a/2a04a2ae3b7214af25318da2be45b4134d571c1b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2a/2a04a2ae3b7214af25318da2be45b4134d571c1b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 WikiRedirectTest < ActiveSupport::TestCase + fixtures :projects, :wikis, :wiki_pages + + def setup + @wiki = Wiki.find(1) + @original = WikiPage.create(:wiki => @wiki, :title => 'Original title') + end + + def test_create_redirect + @original.title = 'New title' + assert @original.save + @original.reload + + assert_equal 'New_title', @original.title + assert @wiki.redirects.find_by_title('Original_title') + assert @wiki.find_page('Original title') + assert @wiki.find_page('ORIGINAL title') + end + + def test_update_redirect + # create a redirect that point to this page + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') + + @original.title = 'New title' + @original.save + # make sure the old page now points to the new page + assert_equal 'New_title', @wiki.find_page('An old page').title + end + + def test_reverse_rename + # create a redirect that point to this page + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') + + @original.title = 'An old page' + @original.save + assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'An_old_page') + assert @wiki.redirects.find_by_title_and_redirects_to('Original_title', 'An_old_page') + end + + def test_rename_to_already_redirected + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Other_page') + + @original.title = 'An old page' + @original.save + # this redirect have to be removed since 'An old page' page now exists + assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'Other_page') + end + + def test_redirects_removed_when_deleting_page + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') + + @original.destroy + assert_nil @wiki.redirects.first + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2a/2a48d77fce2105e4826cccf7c06db5962a047818.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2a/2a48d77fce2105e4826cccf7c06db5962a047818.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,114 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 Tracker < ActiveRecord::Base + + CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze + # Fields that can be disabled + # Other (future) fields should be appended, not inserted! + CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze + + before_destroy :check_integrity + has_many :issues + has_many :workflow_rules, :dependent => :delete_all do + def copy(source_tracker) + WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) + end + end + + has_and_belongs_to_many :projects + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' + acts_as_list + + attr_protected :fields_bits + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + + scope :sorted, lambda { order("#{table_name}.position ASC") } + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + def to_s; name end + + def <=>(tracker) + position <=> tracker.position + end + + # Returns an array of IssueStatus that are used + # in the tracker's workflows + def issue_statuses + if @issue_statuses + return @issue_statuses + elsif new_record? + return [] + end + + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). + flatten. + uniq + @issue_statuses = IssueStatus.where(:id => ids).all.sort + end + + def disabled_core_fields + i = -1 + @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0} + end + + def core_fields + CORE_FIELDS - disabled_core_fields + end + + def core_fields=(fields) + raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array) + + bits = 0 + CORE_FIELDS.each_with_index do |field, i| + unless fields.include?(field) + bits |= 2 ** i + end + end + self.fields_bits = bits + @disabled_core_fields = nil + core_fields + end + + # Returns the fields that are disabled for all the given trackers + def self.disabled_core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:disabled_core_fields).reduce(:&) + else + [] + end + end + + # Returns the fields that are enabled for one tracker at least + def self.core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:core_fields).reduce(:|) + else + CORE_FIELDS.dup + end + end + +private + def check_integrity + raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any? + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2a/2ad19a5d216db9900f8c479e9b99d1592793dd37.svn-base --- a/.svn/pristine/2a/2ad19a5d216db9900f8c479e9b99d1592793dd37.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 Acts - module Event - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def acts_as_event(options = {}) - return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods) - default_options = { :datetime => :created_on, - :title => :title, - :description => :description, - :author => :author, - :url => {:controller => 'welcome'}, - :type => self.name.underscore.dasherize } - - cattr_accessor :event_options - self.event_options = default_options.merge(options) - send :include, Redmine::Acts::Event::InstanceMethods - end - end - - module InstanceMethods - def self.included(base) - base.extend ClassMethods - end - - %w(datetime title description author type).each do |attr| - src = <<-END_SRC - def event_#{attr} - option = event_options[:#{attr}] - if option.is_a?(Proc) - option.call(self) - elsif option.is_a?(Symbol) - send(option) - else - option - end - end - END_SRC - class_eval src, __FILE__, __LINE__ - end - - def event_date - event_datetime.to_date - end - - def event_url(options = {}) - option = event_options[:url] - if option.is_a?(Proc) - option.call(self).merge(options) - elsif option.is_a?(Hash) - option.merge(options) - elsif option.is_a?(Symbol) - send(option).merge(options) - else - option - end - end - - # Returns the mail adresses of users that should be notified - def recipients - notified = project.notified_users - notified.reject! {|user| !visible?(user)} - notified.collect(&:mail) - end - - module ClassMethods - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2a/2adb0876a1493b681532094202fc51bd8794f71f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2a/2adb0876a1493b681532094202fc51bd8794f71f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 UserPreference < ActiveRecord::Base + belongs_to :user + serialize :others + + attr_protected :others, :user_id + + before_save :set_others_hash + + def initialize(attributes=nil, *args) + super + self.others ||= {} + end + + def set_others_hash + self.others ||= {} + end + + def [](attr_name) + if has_attribute? attr_name + super + else + others ? others[attr_name] : nil + end + end + + def []=(attr_name, value) + if has_attribute? attr_name + super + else + h = (read_attribute(:others) || {}).dup + h.update(attr_name => value) + write_attribute(:others, h) + value + end + end + + def comments_sorting; self[:comments_sorting] end + def comments_sorting=(order); self[:comments_sorting]=order end + + def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end + def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end + + def no_self_notified; (self[:no_self_notified] == true || self[:no_self_notified] == '1'); end + def no_self_notified=(value); self[:no_self_notified]=value; end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2b/2b217a0880dd8ada2c9c72a208a8a9c562cb5d2c.svn-base --- a/.svn/pristine/2b/2b217a0880dd8ada2c9c72a208a8a9c562cb5d2c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingAttachmentsTest < ActionController::IntegrationTest - def test_attachments - assert_routing( - { :method => 'get', :path => "/attachments/1" }, - { :controller => 'attachments', :action => 'show', :id => '1' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/1.xml" }, - { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/1.json" }, - { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/1/filename.ext" }, - { :controller => 'attachments', :action => 'show', :id => '1', - :filename => 'filename.ext' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/download/1" }, - { :controller => 'attachments', :action => 'download', :id => '1' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/download/1/filename.ext" }, - { :controller => 'attachments', :action => 'download', :id => '1', - :filename => 'filename.ext' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/thumbnail/1" }, - { :controller => 'attachments', :action => 'thumbnail', :id => '1' } - ) - assert_routing( - { :method => 'get', :path => "/attachments/thumbnail/1/200" }, - { :controller => 'attachments', :action => 'thumbnail', :id => '1', :size => '200' } - ) - assert_routing( - { :method => 'delete', :path => "/attachments/1" }, - { :controller => 'attachments', :action => 'destroy', :id => '1' } - ) - assert_routing( - { :method => 'post', :path => '/uploads.xml' }, - { :controller => 'attachments', :action => 'upload', :format => 'xml' } - ) - assert_routing( - { :method => 'post', :path => '/uploads.json' }, - { :controller => 'attachments', :action => 'upload', :format => 'json' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2b/2b8a016bf78b5250d2e474ce2a1c8d8864231ad4.svn-base --- a/.svn/pristine/2b/2b8a016bf78b5250d2e474ce2a1c8d8864231ad4.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RoutingCustomFieldsTest < ActionController::IntegrationTest - def test_custom_fields - assert_routing( - { :method => 'get', :path => "/custom_fields" }, - { :controller => 'custom_fields', :action => 'index' } - ) - assert_routing( - { :method => 'get', :path => "/custom_fields/new" }, - { :controller => 'custom_fields', :action => 'new' } - ) - assert_routing( - { :method => 'post', :path => "/custom_fields" }, - { :controller => 'custom_fields', :action => 'create' } - ) - assert_routing( - { :method => 'get', :path => "/custom_fields/2/edit" }, - { :controller => 'custom_fields', :action => 'edit', :id => '2' } - ) - assert_routing( - { :method => 'put', :path => "/custom_fields/2" }, - { :controller => 'custom_fields', :action => 'update', :id => '2' } - ) - assert_routing( - { :method => 'delete', :path => "/custom_fields/2" }, - { :controller => 'custom_fields', :action => 'destroy', :id => '2' } - ) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2b/2b98859b11810ab4410410a5454271ee65d2638c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2b/2b98859b11810ab4410410a5454271ee65d2638c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,95 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 Activity + # Class used to retrieve activity events + class Fetcher + attr_reader :user, :project, :scope + + # Needs to be unloaded in development mode + @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } } + + def initialize(user, options={}) + options.assert_valid_keys(:project, :with_subprojects, :author) + @user = user + @project = options[:project] + @options = options + + @scope = event_types + end + + # Returns an array of available event types + def event_types + return @event_types unless @event_types.nil? + + @event_types = Redmine::Activity.available_event_types + @event_types = @event_types.select {|o| @project.self_and_descendants.detect {|p| @user.allowed_to?("view_#{o}".to_sym, p)}} if @project + @event_types + end + + # Yields to filter the activity scope + def scope_select(&block) + @scope = @scope.select {|t| yield t } + end + + # Sets the scope + # Argument can be :all, :default or an array of event types + def scope=(s) + case s + when :all + @scope = event_types + when :default + default_scope! + else + @scope = s & event_types + end + end + + # Resets the scope to the default scope + def default_scope! + @scope = Redmine::Activity.default_event_types + end + + # Returns an array of events for the given date range + # sorted in reverse chronological order + def events(from = nil, to = nil, options={}) + e = [] + @options[:limit] = options[:limit] + + @scope.each do |event_type| + constantized_providers(event_type).each do |provider| + e += provider.find_events(event_type, @user, from, to, @options) + end + end + + e.sort! {|a,b| b.event_datetime <=> a.event_datetime} + + if options[:limit] + e = e.slice(0, options[:limit]) + end + e + end + + private + + def constantized_providers(event_type) + @@constantized_providers[event_type] + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2b/2bdd14c51d14eeccec1ac400b671772bf40793e4.svn-base --- a/.svn/pristine/2b/2bdd14c51d14eeccec1ac400b671772bf40793e4.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1084 +0,0 @@ -hr: - direction: ltr - 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: "%m/%d/%Y" - short: "%b %d" - long: "%B %d, %Y" - - day_names: [Ponedjeljak, Utorak, Srijeda, ÄŒetvrtak, Petak, Subota, Nedjelja] - abbr_day_names: [Ned, Pon, Uto, Sri, ÄŒet, Pet, Sub] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, Sijecanj, Veljaca, Ožujak, Travanj, Svibanj, Lipanj, Srpanj, Kolovoz, Rujan, Listopad, Studeni, Prosinac] - abbr_month_names: [~, Sij, Velj, Ožu, Tra, Svi, Lip, Srp, Kol, Ruj, List, Stu, Pro] - # Used in date_select and datime_select. - order: - - :year - - :month - - :day - - time: - formats: - default: "%m/%d/%Y %I:%M %p" - time: "%I:%M %p" - short: "%d %b %H:%M" - long: "%B %d, %Y %H:%M" - am: "am" - pm: "pm" - - datetime: - distance_in_words: - half_a_minute: "pola minute" - less_than_x_seconds: - one: "manje od sekunde" - other: "manje od %{count} sekundi" - x_seconds: - one: "1 sekunda" - other: "%{count} sekundi" - less_than_x_minutes: - one: "manje od minute" - other: "manje od %{count} minuta" - x_minutes: - one: "1 minuta" - other: "%{count} minuta" - about_x_hours: - one: "oko sat vremena" - other: "oko %{count} sati" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "1 dan" - other: "%{count} dana" - about_x_months: - one: "oko 1 mjesec" - other: "oko %{count} mjeseci" - x_months: - one: "mjesec" - other: "%{count} mjeseci" - about_x_years: - one: "1 godina" - other: "%{count} godina" - over_x_years: - one: "preko 1 godine" - other: "preko %{count} godina" - - number: - 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: "i" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "1 error prohibited this %{model} from being saved" - other: "%{count} errors prohibited this %{model} from being saved" - messages: - inclusion: "nije ukljuceno u listu" - exclusion: "je rezervirano" - invalid: "nije ispravno" - confirmation: "ne odgovara za potvrdu" - accepted: "mora biti prihvaćen" - empty: "ne može biti prazno" - blank: "ne može biti razmaka" - too_long: "je predug (maximum is %{count} characters)" - too_short: "je prekratak (minimum is %{count} characters)" - wrong_length: "je pogreÅ¡ne dužine (should be %{count} characters)" - taken: "već je zauzeto" - not_a_number: "nije broj" - not_a_date: "nije ispravan datum" - greater_than: "mora biti veći od %{count}" - greater_than_or_equal_to: "mora biti veći ili jednak %{count}" - equal_to: "mora biti jednak %{count}" - less_than: "mora biti manji od %{count}" - less_than_or_equal_to: "mora bit manji ili jednak%{count}" - odd: "mora biti neparan" - even: "mora biti paran" - greater_than_start_date: "mora biti veci nego pocetni datum" - not_same_project: "ne pripada istom projektu" - circular_dependency: "Ovaj relacija stvara kružnu ovisnost" - cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" - - actionview_instancetag_blank_option: Molimo odaberite - - general_text_No: 'Ne' - general_text_Yes: 'Da' - general_text_no: 'ne' - general_text_yes: 'da' - general_lang_name: 'Hrvatski' - general_csv_separator: ',' - general_csv_decimal_separator: '.' - general_csv_encoding: UTF-8 - general_pdf_encoding: UTF-8 - general_first_day_of_week: '7' - - notice_account_updated: VaÅ¡ profil je uspjeÅ¡no promijenjen. - notice_account_invalid_creditentials: Neispravno korisniÄko ime ili zaporka. - notice_account_password_updated: Zaporka je uspjeÅ¡no promijenjena. - notice_account_wrong_password: PogreÅ¡na zaporka - notice_account_register_done: Racun je uspjeÅ¡no napravljen. Da biste aktivirali svoj raÄun, kliknite na link koji vam je poslan na e-mail. - notice_account_unknown_email: Nepoznati korisnik. - notice_can_t_change_password: Ovaj raÄun koristi eksterni izvor prijavljivanja. Nemoguće je promijeniti zaporku. - notice_account_lost_email_sent: E-mail s uputama kako bi odabrali novu zaporku je poslan na na vaÅ¡u e-mail adresu. - notice_account_activated: VaÅ¡ racun je aktiviran. Možete se prijaviti. - notice_successful_create: UspjeÅ¡no napravljeno. - notice_successful_update: UspjeÅ¡na promjena. - notice_successful_delete: UspjeÅ¡no brisanje. - notice_successful_connection: UspjeÅ¡na veza. - notice_file_not_found: Stranica kojoj ste pokuÅ¡ali pristupiti ne postoji ili je uklonjena. - notice_locking_conflict: Podataci su ažurirani od strane drugog korisnika. - notice_not_authorized: Niste ovlaÅ¡teni za pristup ovoj stranici. - notice_email_sent: E-mail je poslan %{value}" - notice_email_error: Dogodila se pogreÅ¡ka tijekom slanja E-maila (%{value})" - notice_feeds_access_key_reseted: VaÅ¡ RSS pristup je resetovan. - notice_api_access_key_reseted: VaÅ¡ API pristup je resetovan. - notice_failed_to_save_issues: "Neuspjelo spremanje %{count} predmeta na %{total} odabrane: %{ids}." - notice_no_issue_selected: "Niti jedan predmet nije odabran! Molim, odaberite predmete koje želite urediti." - notice_account_pending: "VaÅ¡ korisnicki raÄun je otvoren, Äeka odobrenje administratora." - notice_default_data_loaded: Konfiguracija je uspjeÅ¡no uÄitana. - notice_unable_delete_version: Nije moguće izbrisati verziju. - notice_issue_done_ratios_updated: Issue done ratios updated. - - error_can_t_load_default_data: "Zadanu konfiguracija nije uÄitana: %{value}" - error_scm_not_found: "Unos i/ili revizija nije pronaÄ‘en." - error_scm_command_failed: "Dogodila se pogreÅ¡ka prilikom pokuÅ¡aja pristupa: %{value}" - error_scm_annotate: "Ne postoji ili ne može biti obilježen." - error_issue_not_found_in_project: 'Nije pronaÄ‘en ili ne pripada u ovaj projekt' - error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.' - error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' - error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened' - error_can_not_archive_project: This project can not be archived - error_issue_done_ratios_not_updated: "Issue done ratios not updated." - error_workflow_copy_source: 'Please select a source tracker or role' - error_workflow_copy_target: 'Please select target tracker(s) and role(s)' - - warning_attachments_not_saved: "%{count} Datoteka/e nije mogla biti spremljena." - - mail_subject_lost_password: "VaÅ¡a %{value} zaporka" - mail_body_lost_password: 'Kako biste promijenili VaÅ¡u zaporku slijedite poveznicu:' - mail_subject_register: "Aktivacija korisniÄog raÄuna %{value}" - mail_body_register: 'Da biste aktivirali svoj raÄun, kliknite na sljedeci link:' - mail_body_account_information_external: "Možete koristiti vaÅ¡ raÄun %{value} za prijavu." - mail_body_account_information: VaÅ¡i korisniÄki podaci - mail_subject_account_activation_request: "%{value} predmet za aktivaciju korisniÄkog raÄuna" - mail_body_account_activation_request: "Novi korisnik (%{value}) je registriran. Njegov korisniÄki raÄun Äeka vaÅ¡e odobrenje:" - mail_subject_reminder: "%{count} predmet(a) dospijeva sljedećih %{days} dana" - mail_body_reminder: "%{count} vama dodijeljen(ih) predmet(a) dospijeva u sljedećih %{days} dana:" - mail_subject_wiki_content_added: "'%{id}' wiki page has been added" - mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}." - mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" - mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}." - - gui_validation_error: 1 pogreÅ¡ka - gui_validation_error_plural: "%{count} pogreÅ¡aka" - - field_name: Ime - field_description: Opis - field_summary: Sažetak - field_is_required: Obavezno - field_firstname: Ime - field_lastname: Prezime - field_mail: E-poÅ¡ta - field_filename: Datoteka - field_filesize: VeliÄina - field_downloads: Preuzimanja - field_author: Autor - field_created_on: Napravljen - field_updated_on: Promijenjen - field_field_format: Format - field_is_for_all: Za sve projekte - field_possible_values: Moguće vrijednosti - field_regexp: Regularni izraz - field_min_length: Minimalna dužina - field_max_length: Maksimalna dužina - field_value: Vrijednost - field_category: Kategorija - field_title: Naslov - field_project: Projekt - field_issue: Predmet - field_status: Status - field_notes: Napomene - field_is_closed: Predmet je zatvoren - field_is_default: Zadana vrijednost - field_tracker: Tracker - field_subject: Predmet - field_due_date: Do datuma - field_assigned_to: Dodijeljeno - field_priority: Prioritet - field_fixed_version: Verzija - field_user: Korisnik - field_role: Uloga - field_homepage: Naslovnica - field_is_public: Javni projekt - field_parent: Potprojekt od - field_is_in_roadmap: Predmeti se prikazuju u Putokazu - field_login: KorisniÄko ime - field_mail_notification: Obavijest putem e-poÅ¡te - field_admin: Administrator - field_last_login_on: Zadnja prijava - field_language: Primarni jezik - field_effective_date: Datum - field_password: Zaporka - field_new_password: Nova zaporka - field_password_confirmation: Potvrda zaporke - field_version: Verzija - field_type: Tip - field_host: Host - field_port: Port - field_account: Racun - field_base_dn: Osnovni DN - field_attr_login: Login atribut - field_attr_firstname: Atribut imena - field_attr_lastname: Atribut prezimena - field_attr_mail: Atribut e-poÅ¡te - field_onthefly: "Izrada korisnika \"u hodu\"" - field_start_date: Pocetak - field_done_ratio: "% UÄinjeno" - field_auth_source: Vrsta prijavljivanja - field_hide_mail: Sakrij moju adresu e-poÅ¡te - field_comments: Komentar - field_url: URL - field_start_page: PoÄetna stranica - field_subproject: Potprojekt - field_hours: Sati - field_activity: Aktivnost - field_spent_on: Datum - field_identifier: Identifikator - field_is_filter: KoriÅ¡teno kao filtar - field_issue_to_id: Povezano s predmetom - field_delay: Odgodeno - field_assignable: Predmeti mogu biti dodijeljeni ovoj ulozi - field_redirect_existing_links: Preusmjeravanje postojećih linkova - field_estimated_hours: Procijenjeno vrijeme - field_column_names: Stupci - field_time_zone: Vremenska zona - field_searchable: Pretraživo - field_default_value: Zadana vrijednost - field_comments_sorting: Prikaz komentara - field_parent_title: Parent page - field_editable: Editable - field_watcher: Watcher - field_identity_url: OpenID URL - field_content: Content - field_group_by: Group results by - - setting_app_title: Naziv aplikacije - setting_app_subtitle: Podnaslov aplikacije - setting_welcome_text: Tekst dobrodoÅ¡lice - setting_default_language: Zadani jezik - setting_login_required: Potrebna je prijava - setting_self_registration: Samoregistracija je dozvoljena - setting_attachment_max_size: Maksimalna veliÄina privitka - setting_issues_export_limit: OgraniÄenje izvoza predmeta - setting_mail_from: Izvorna adresa e-poÅ¡te - setting_bcc_recipients: Blind carbon copy primatelja (bcc) - setting_plain_text_mail: obiÄni tekst poÅ¡te (bez HTML-a) - setting_host_name: Naziv domaćina (host) - setting_text_formatting: Oblikovanje teksta - setting_wiki_compression: Sažimanje - setting_feeds_limit: Ogranicenje unosa sadržaja - setting_default_projects_public: Novi projekti su javni po defaultu - setting_autofetch_changesets: Autofetch commits - setting_sys_api_enabled: Omogući WS za upravljanje skladiÅ¡tem - setting_commit_ref_keywords: Referentne kljuÄne rijeÄi - setting_commit_fix_keywords: Fiksne kljuÄne rijeÄi - setting_autologin: Automatska prijava - setting_date_format: Format datuma - setting_time_format: Format vremena - setting_cross_project_issue_relations: Dozvoli povezivanje predmeta izmedu razliÄitih projekata - setting_issue_list_default_columns: Stupci prikazani na listi predmeta - setting_emails_footer: Zaglavlje e-poÅ¡te - setting_protocol: Protokol - setting_per_page_options: Objekata po stranici opcija - setting_user_format: Oblik prikaza korisnika - setting_activity_days_default: Dani prikazane aktivnosti na projektu - setting_display_subprojects_issues: Prikaz predmeta potprojekta na glavnom projektu po defaultu - setting_enabled_scm: Omogućen SCM - setting_mail_handler_body_delimiters: "Truncate emails after one of these lines" - setting_mail_handler_api_enabled: Omoguci WS za dolaznu e-poÅ¡tu - setting_mail_handler_api_key: API kljuÄ - setting_sequential_project_identifiers: Generiraj slijedne identifikatore projekta - setting_gravatar_enabled: Koristi Gravatar korisniÄke ikone - setting_gravatar_default: Default Gravatar image - setting_diff_max_lines_displayed: Maksimalni broj diff linija za prikazati - setting_file_max_size_displayed: Max size of text files displayed inline - setting_repository_log_display_limit: Maximum number of revisions displayed on file log - setting_openid: Allow OpenID login and registration - setting_password_min_length: Minimum password length - setting_new_project_user_role_id: Role given to a non-admin user who creates a project - setting_default_projects_modules: Default enabled modules for new projects - setting_issue_done_ratio: Calculate the issue done ratio with - setting_issue_done_ratio_issue_field: Use the issue field - setting_issue_done_ratio_issue_status: Use the issue status - setting_start_of_week: Start calendars on - setting_rest_api_enabled: Enable REST web service - - permission_add_project: Dodaj projekt - permission_add_subprojects: Dodaj potprojekt - permission_edit_project: Uredi projekt - permission_select_project_modules: Odaberi projektne module - permission_manage_members: Upravljaj Älanovima - permission_manage_versions: Upravljaj verzijama - permission_manage_categories: Upravljaj kategorijama predmeta - permission_view_issues: Pregledaj zahtjeve - permission_add_issues: Dodaj predmete - permission_edit_issues: Uredi predmete - permission_manage_issue_relations: Upravljaj relacijama predmeta - permission_add_issue_notes: Dodaj biljeÅ¡ke - permission_edit_issue_notes: Uredi biljeÅ¡ke - permission_edit_own_issue_notes: Uredi vlastite biljeÅ¡ke - permission_move_issues: Premjesti predmete - permission_delete_issues: Brisanje predmeta - permission_manage_public_queries: Upravljaj javnim upitima - permission_save_queries: Spremi upite - permission_view_gantt: Pregledaj gantt grafikon - permission_view_calendar: Pregledaj kalendar - permission_view_issue_watchers: Pregledaj listu promatraca - permission_add_issue_watchers: Dodaj promatraÄa - permission_delete_issue_watchers: Delete watchers - permission_log_time: Dnevnik utroÅ¡enog vremena - permission_view_time_entries: Pregledaj utroÅ¡eno vrijeme - permission_edit_time_entries: Uredi vremenske dnevnike - permission_edit_own_time_entries: Edit own time logs - permission_manage_news: Upravljaj novostima - permission_comment_news: Komentiraj novosti - permission_manage_documents: Upravljaj dokumentima - permission_view_documents: Pregledaj dokumente - permission_manage_files: Upravljaj datotekama - permission_view_files: Pregledaj datoteke - permission_manage_wiki: Upravljaj wikijem - permission_rename_wiki_pages: Promijeni ime wiki stranicama - permission_delete_wiki_pages: ObriÅ¡i wiki stranice - permission_view_wiki_pages: Pregledaj wiki - permission_view_wiki_edits: Pregledaj povijest wikija - permission_edit_wiki_pages: Uredi wiki stranice - permission_delete_wiki_pages_attachments: ObriÅ¡i privitke - permission_protect_wiki_pages: ZaÅ¡titi wiki stranice - permission_manage_repository: Upravljaj skladiÅ¡tem - permission_browse_repository: Browse repository - permission_view_changesets: View changesets - permission_commit_access: Mogućnost pohranjivanja - permission_manage_boards: Manage boards - permission_view_messages: Pregledaj poruke - permission_add_messages: Objavi poruke - permission_edit_messages: Uredi poruke - permission_edit_own_messages: Uredi vlastite poruke - permission_delete_messages: ObriÅ¡i poruke - permission_delete_own_messages: ObriÅ¡i vlastite poruke - - project_module_issue_tracking: Praćenje predmeta - project_module_time_tracking: Praćenje vremena - project_module_news: Novosti - project_module_documents: Dokumenti - project_module_files: Datoteke - project_module_wiki: Wiki - project_module_repository: SkladiÅ¡te - project_module_boards: Boards - - label_user: Korisnik - label_user_plural: Korisnici - label_user_new: Novi korisnik - label_user_anonymous: Anonymous - label_project: Projekt - label_project_new: Novi projekt - label_project_plural: Projekti - label_x_projects: - zero: no projects - one: 1 project - other: "%{count} projects" - label_project_all: Svi Projekti - label_project_latest: Najnoviji projekt - label_issue: Predmet - label_issue_new: Novi predmet - label_issue_plural: Predmeti - label_issue_view_all: Pregled svih predmeta - label_issues_by: "Predmeti od %{value}" - label_issue_added: Predmet dodan - label_issue_updated: Predmet promijenjen - label_document: Dokument - label_document_new: Novi dokument - label_document_plural: Dokumenti - label_document_added: Dokument dodan - label_role: Uloga - label_role_plural: Uloge - label_role_new: Nova uloga - label_role_and_permissions: Uloge i ovlasti - label_member: ÄŒlan - label_member_new: Novi Älan - label_member_plural: ÄŒlanovi - label_tracker: Vrsta - label_tracker_plural: Vrste predmeta - label_tracker_new: Nova vrsta - label_workflow: Tijek rada - label_issue_status: Status predmeta - label_issue_status_plural: Status predmeta - label_issue_status_new: Novi status - label_issue_category: Kategorija predmeta - label_issue_category_plural: Kategorije predmeta - label_issue_category_new: Nova kategorija - label_custom_field: KorisniÄki definirano polje - label_custom_field_plural: KorisniÄki definirana polja - label_custom_field_new: Novo korisniÄki definirano polje - label_enumerations: Pobrojenice - label_enumeration_new: Nova vrijednost - label_information: Informacija - label_information_plural: Informacije - label_please_login: Molim prijavite se - label_register: Registracija - label_login_with_open_id_option: or login with OpenID - label_password_lost: Izgubljena zaporka - label_home: PoÄetna stranica - label_my_page: Moja stranica - label_my_account: Moj profil - label_my_projects: Moji projekti - label_administration: Administracija - label_login: Korisnik - label_logout: Odjava - label_help: Pomoć - label_reported_issues: Prijavljeni predmeti - label_assigned_to_me_issues: Moji predmeti - label_last_login: Last connection - label_registered_on: Registrirano - label_activity: Aktivnosti - label_overall_activity: Aktivnosti - label_user_activity: "%{value} ova/ina aktivnost" - label_new: Novi - label_logged_as: Prijavljeni ste kao - label_environment: Okolina - label_authentication: Autentikacija - label_auth_source: NaÄin prijavljivanja - label_auth_source_new: Novi naÄin prijavljivanja - label_auth_source_plural: NaÄini prijavljivanja - label_subproject_plural: Potprojekti - label_subproject_new: Novi potprojekt - label_and_its_subprojects: "%{value} i njegovi potprojekti" - label_min_max_length: Min - Maks veliÄina - label_list: Liste - label_date: Datum - label_integer: Integer - label_float: Float - label_boolean: Boolean - label_string: Text - label_text: Long text - label_attribute: Atribut - label_attribute_plural: Atributi - label_download: "%{count} Download" - label_download_plural: "%{count} Downloads" - label_no_data: Nema podataka za prikaz - label_change_status: Promjena statusa - label_history: Povijest - label_attachment: Datoteka - label_attachment_new: Nova datoteka - label_attachment_delete: Brisanje datoteke - label_attachment_plural: Datoteke - label_file_added: Datoteka dodana - label_report: Izvješće - label_report_plural: Izvješća - label_news: Novosti - label_news_new: Dodaj novost - label_news_plural: Novosti - label_news_latest: Novosti - label_news_view_all: Pregled svih novosti - label_news_added: Novosti dodane - label_settings: Postavke - label_overview: Pregled - label_version: Verzija - label_version_new: Nova verzija - label_version_plural: Verzije - label_confirmation: Potvrda - label_export_to: 'Izvoz u:' - label_read: ÄŒitaj... - label_public_projects: Javni projekti - label_open_issues: Otvoren - label_open_issues_plural: Otvoreno - label_closed_issues: Zatvoren - label_closed_issues_plural: Zatvoreno - label_x_open_issues_abbr_on_total: - zero: 0 open / %{total} - one: 1 open / %{total} - other: "%{count} open / %{total}" - label_x_open_issues_abbr: - zero: 0 open - one: 1 open - other: "%{count} open" - label_x_closed_issues_abbr: - zero: 0 closed - one: 1 closed - other: "%{count} closed" - label_total: Ukupno - label_permissions: Dozvole - label_current_status: Trenutni status - label_new_statuses_allowed: Novi status je dozvoljen - label_all: Svi - label_none: nema - label_nobody: nitko - label_next: Naredni - label_previous: Prethodni - label_used_by: KoriÅ¡ten od - label_details: Detalji - label_add_note: Dodaj napomenu - label_per_page: Po stranici - label_calendar: Kalendar - label_months_from: Mjeseci od - label_gantt: Gantt - label_internal: Interno - label_last_changes: "Posljednjih %{count} promjena" - label_change_view_all: Prikaz svih promjena - label_personalize_page: Prilagodite ovu stranicu - label_comment: Komentar - label_comment_plural: Komentari - label_x_comments: - zero: no comments - one: 1 comment - other: "%{count} comments" - label_comment_add: Dodaj komentar - label_comment_added: Komentar dodan - label_comment_delete: Brisanje komentara - label_query: KorisniÄki upit - label_query_plural: KorisniÄki upiti - label_query_new: Novi upit - label_filter_add: Dodaj filtar - label_filter_plural: Filtri - label_equals: je - label_not_equals: nije - label_in_less_than: za manje od - label_in_more_than: za viÅ¡e od - label_greater_or_equal: '>=' - label_less_or_equal: '<=' - label_in: za toÄno - label_today: danas - label_all_time: sva vremena - label_yesterday: juÄer - label_this_week: ovog tjedna - label_last_week: proÅ¡log tjedna - label_last_n_days: "zadnjih %{count} dana" - label_this_month: ovog mjeseca - label_last_month: proÅ¡log mjeseca - label_this_year: ove godine - label_date_range: vremenski raspon - label_less_than_ago: manje od - label_more_than_ago: viÅ¡e od - label_ago: prije - label_contains: Sadrži - label_not_contains: ne sadrži - label_day_plural: dana - label_repository: SkladiÅ¡te - label_repository_plural: SkladiÅ¡ta - label_browse: Pregled - label_modification: "%{count} promjena" - label_modification_plural: "%{count} promjena" - label_branch: Branch - label_tag: Tag - label_revision: Revizija - label_revision_plural: Revizije - label_revision_id: "Revision %{value}" - label_associated_revisions: Dodijeljene revizije - label_added: dodano - label_modified: promijenjen - label_copied: kopirano - label_renamed: preimenovano - label_deleted: obrisano - label_latest_revision: Posljednja revizija - label_latest_revision_plural: Posljednje revizije - label_view_revisions: Pregled revizija - label_view_all_revisions: View all revisions - label_max_size: Maksimalna veliÄina - label_sort_highest: Premjesti na vrh - label_sort_higher: Premjesti prema gore - label_sort_lower: Premjesti prema dolje - label_sort_lowest: Premjesti na dno - label_roadmap: Putokaz - label_roadmap_due_in: "ZavrÅ¡ava se za %{value}" - label_roadmap_overdue: "%{value} kasni" - label_roadmap_no_issues: Nema predmeta za ovu verziju - label_search: Traži - label_result_plural: Rezultati - label_all_words: Sve rijeÄi - label_wiki: Wiki - label_wiki_edit: Wiki promjena - label_wiki_edit_plural: Wiki promjene - label_wiki_page: Wiki stranica - label_wiki_page_plural: Wiki stranice - label_index_by_title: Indeks po naslovima - label_index_by_date: Indeks po datumu - label_current_version: Trenutna verzija - label_preview: Brzi pregled - label_feed_plural: Feeds - label_changes_details: Detalji svih promjena - label_issue_tracking: Praćenje predmeta - label_spent_time: UtroÅ¡eno vrijeme - label_f_hour: "%{value} sata" - label_f_hour_plural: "%{value} sati" - label_time_tracking: Praćenje vremena - label_change_plural: Promjene - label_statistics: Statistika - label_commits_per_month: Pohrana po mjesecu - label_commits_per_author: Pohrana po autoru - label_view_diff: Pregled razlika - label_diff_inline: uvuÄeno - label_diff_side_by_side: paralelno - label_options: Opcije - label_copy_workflow_from: Kopiraj tijek rada od - label_permissions_report: Izvješće o dozvolama - label_watched_issues: Praćeni predmeti - label_related_issues: Povezani predmeti - label_applied_status: Primijenjen status - label_loading: UÄitavam... - label_relation_new: Nova relacija - label_relation_delete: Brisanje relacije - label_relates_to: u relaciji sa - label_duplicates: Duplira - label_duplicated_by: ponovljen kao - label_blocks: blokira - label_blocked_by: blokiran od strane - label_precedes: prethodi - label_follows: slijedi - label_end_to_start: od kraja do poÄetka - label_end_to_end: od kraja do kraja - label_end_to_start: od kraja do poÄetka - label_end_to_end: od kraja do kraja - label_stay_logged_in: Ostanite prijavljeni - label_disabled: IskljuÄen - label_show_completed_versions: Prikaži zavrÅ¡ene verzije - label_me: ja - label_board: Forum - label_board_new: Novi forum - label_board_plural: Forumi - label_topic_plural: Teme - label_message_plural: Poruke - label_message_last: Posljednja poruka - label_message_new: Nova poruka - label_message_posted: Poruka dodana - label_reply_plural: Odgovori - label_send_information: PoÅ¡alji korisniku informaciju o profilu - label_year: Godina - label_month: Mjesec - label_week: Tjedan - label_date_from: Od - label_date_to: Do - label_language_based: Zasnovano na jeziku - label_sort_by: "Uredi po %{value}" - label_send_test_email: PoÅ¡alji testno E-pismo - label_feeds_access_key: RSS access key - label_missing_feeds_access_key: Missing a RSS access key - label_feeds_access_key_created_on: "RSS kljuc za pristup je napravljen prije %{value}" - label_module_plural: Moduli - label_added_time_by: "Promijenio %{author} prije %{age}" - label_updated_time_by: "Dodao/la %{author} prije %{age}" - label_updated_time: "Promijenjeno prije %{value}" - label_jump_to_a_project: Prebaci se na projekt... - label_file_plural: Datoteke - label_changeset_plural: Promjene - label_default_columns: Zadani stupci - label_no_change_option: (Bez promjene) - label_bulk_edit_selected_issues: ZajedniÄka promjena izabranih predmeta - label_theme: Tema - label_default: Zadana - label_search_titles_only: Pretraživanje samo naslova - label_user_mail_option_all: "Za bilo koji dogaÄ‘aj na svim mojim projektima" - label_user_mail_option_selected: "Za bilo koji dogaÄ‘aj samo za izabrane projekte..." - label_user_mail_no_self_notified: "Ne želim primati obavijesti o promjenama koje sam napravim" - label_registration_activation_by_email: aktivacija putem e-poÅ¡te - label_registration_manual_activation: ruÄna aktivacija - label_registration_automatic_activation: automatska aktivacija - label_display_per_page: "Po stranici: %{value}" - label_age: Starost - label_change_properties: Promijeni svojstva - label_general: Općenito - label_more: JoÅ¡ - label_scm: SCM - label_plugins: Plugins - label_ldap_authentication: LDAP autentikacija - label_downloads_abbr: D/L - label_optional_description: Opcije - label_add_another_file: Dodaj joÅ¡ jednu datoteku - label_preferences: Preferences - label_chronological_order: U kronoloÅ¡kom redoslijedu - label_reverse_chronological_order: U obrnutom kronoloÅ¡kom redoslijedu - label_planning: Planiranje - label_incoming_emails: Dolazne poruke e-poÅ¡te - label_generate_key: Generiraj kljuÄ - label_issue_watchers: PromatraÄi - label_example: Primjer - label_display: Display - label_sort: Sort - label_ascending: Ascending - label_descending: Descending - label_date_from_to: From %{start} to %{end} - label_wiki_content_added: Wiki page added - label_wiki_content_updated: Wiki page updated - label_group: Group - label_group_plural: Grupe - label_group_new: Nova grupa - label_time_entry_plural: Spent time - label_version_sharing_none: Not shared - label_version_sharing_descendants: With subprojects - label_version_sharing_hierarchy: With project hierarchy - label_version_sharing_tree: With project tree - label_version_sharing_system: With all projects - label_update_issue_done_ratios: Update issue done ratios - label_copy_source: Source - label_copy_target: Target - label_copy_same_as_target: Same as target - label_display_used_statuses_only: Only display statuses that are used by this tracker - label_api_access_key: API access key - label_missing_api_access_key: Missing an API access key - label_api_access_key_created_on: "API access key created %{value} ago" - - button_login: Prijavi - button_submit: PoÅ¡alji - button_save: Spremi - button_check_all: OznaÄi sve - button_uncheck_all: IskljuÄi sve - button_delete: ObriÅ¡i - button_create: Napravi - button_create_and_continue: Napravi i nastavi - button_test: Test - button_edit: Uredi - button_add: Dodaj - button_change: Promijeni - button_apply: Primijeni - button_clear: Ukloni - button_lock: ZakljuÄaj - button_unlock: OtkljuÄaj - button_download: Preuzmi - button_list: Spisak - button_view: Pregled - button_move: Premjesti - button_move_and_follow: Move and follow - button_back: Nazad - button_cancel: Odustani - button_activate: Aktiviraj - button_sort: Redoslijed - button_log_time: ZapiÅ¡i vrijeme - button_rollback: IzvrÅ¡i rollback na ovu verziju - button_watch: Prati - button_unwatch: Prekini pracenje - button_reply: Odgovori - button_archive: Arhiviraj - button_rollback: Dearhiviraj - button_reset: PoniÅ¡ti - button_rename: Promijeni ime - button_change_password: Promjena zaporke - button_copy: Kopiraj - button_copy_and_follow: Copy and follow - button_annotate: Annotate - button_update: Promijeni - button_configure: Konfiguracija - button_quote: Navod - button_duplicate: Duplicate - button_show: Show - - status_active: aktivan - status_registered: Registriran - status_locked: zakljuÄan - - version_status_open: open - version_status_locked: locked - version_status_closed: closed - - field_active: Active - - text_select_mail_notifications: Izbor akcija za koje će biti poslana obavijest e-poÅ¡tom. - text_regexp_info: eg. ^[A-Z0-9]+$ - text_min_max_length_info: 0 znaÄi bez ograniÄenja - text_project_destroy_confirmation: Da li ste sigurni da želite izbrisati ovaj projekt i sve njegove podatke? - text_subprojects_destroy_warning: "Njegov(i) potprojekt(i): %{value} će takoÄ‘er biti obrisan." - text_workflow_edit: Select a role and a tracker to edit the workflow - text_are_you_sure: Da li ste sigurni? - text_journal_changed: "%{label} promijenjen iz %{old} u %{new}" - text_journal_set_to: "%{label} postavi na %{value}" - text_journal_deleted: "%{label} izbrisano (%{old})" - text_journal_added: "%{label} %{value} added" - text_tip_issue_begin_day: Zadaci koji poÄinju ovog dana - text_tip_issue_end_day: zadaci koji se zavrÅ¡avaju ovog dana - text_tip_issue_begin_end_day: Zadaci koji poÄinju i zavrÅ¡avaju se ovog dana - text_caracters_maximum: "NajviÅ¡e %{count} znakova." - text_caracters_minimum: "Mora biti dugaÄko najmanje %{count} znakova." - text_length_between: "Dužina izmedu %{min} i %{max} znakova." - text_tracker_no_workflow: Tijek rada nije definiran za ovaj tracker - text_unallowed_characters: Nedozvoljeni znakovi - text_comma_separated: ViÅ¡estruke vrijednosti su dozvoljene (razdvojene zarezom). - text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages - text_tracker_no_workflow: No workflow defined for this tracker - text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages - text_issue_added: "Predmet %{id} je prijavljen (prijavio %{author})." - text_issue_updated: "Predmet %{id} je promijenjen %{author})." - text_wiki_destroy_confirmation: Da li ste sigurni da želite izbrisati ovaj wiki i njegov sadržaj? - text_issue_category_destroy_question: "Neke predmeti (%{count}) su dodijeljeni ovoj kategoriji. Å to želite uraditi?" - text_issue_category_destroy_assignments: Ukloni dodjeljivanje kategorija - text_issue_category_reassign_to: Ponovo dodijeli predmete ovoj kategoriji - text_user_mail_option: "Za neizabrane projekte, primit ćete obavjesti samo o stvarima koje pratite ili u kojima sudjelujete (npr. predmete koje ste vi napravili ili koje su vama dodjeljeni)." - text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." - text_load_default_configuration: UÄitaj poÄetnu konfiguraciju - text_status_changed_by_changeset: "Applied in changeset %{value}." - text_issues_destroy_confirmation: 'Jeste li sigurni da želite obrisati izabrani/e predmet(e)?' - text_select_project_modules: 'Odaberite module koji će biti omogućeni za ovaj projekt:' - text_default_administrator_account_changed: Default administrator account changed - text_file_repository_writable: Dozvoljeno pisanje u direktorij za privitke - text_plugin_assets_writable: Plugin assets directory writable - text_rmagick_available: RMagick dostupan (nije obavezno) - text_destroy_time_entries_question: "%{hours} sati je prijavljeno za predmete koje želite obrisati. Å to ćete uÄiniti?" - text_destroy_time_entries: ObriÅ¡i prijavljene sate - text_assign_time_entries_to_project: Pridruži prijavljene sate projektu - text_reassign_time_entries: 'Premjesti prijavljene sate ovom predmetu:' - text_user_wrote: "%{value} je napisao/la:" - text_enumeration_destroy_question: "%{count} objekata je pridruženo toj vrijednosti." - text_enumeration_category_reassign_to: 'Premjesti ih ovoj vrijednosti:' - text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." - text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." - text_diff_truncated: '... Ovaj diff je odrezan zato Å¡to prelazi maksimalnu veliÄinu koja može biti prikazana.' - text_custom_field_possible_values_info: 'One line for each value' - text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?" - text_wiki_page_nullify_children: "Keep child pages as root pages" - text_wiki_page_destroy_children: "Delete child pages and all their descendants" - text_wiki_page_reassign_children: "Reassign child pages to this parent page" - default_role_manager: Upravitelj - default_role_developer: Razvojni inženjer - default_role_reporter: Korisnik - default_tracker_bug: PogreÅ¡ka - default_tracker_feature: Funkcionalnost - default_tracker_support: PodrÅ¡ka - default_issue_status_new: Novo - default_issue_status_assigned: Dodijeljeno - default_issue_status_resolved: RijeÅ¡eno - default_issue_status_feedback: Povratna informacija - default_issue_status_closed: Zatvoreno - default_issue_status_rejected: Odbaceno - default_doc_category_user: KorisniÄka dokumentacija - default_doc_category_tech: TehniÄka dokumentacija - default_priority_low: Nizak - default_priority_normal: Redovan - default_priority_high: Visok - default_priority_urgent: Hitan - default_priority_immediate: Odmah - default_activity_design: Dizajn - default_activity_development: Razvoj - enumeration_issue_priorities: Prioriteti predmeta - enumeration_doc_categories: Kategorija dokumenata - enumeration_activities: Aktivnosti (po vremenu) - enumeration_system_activity: System Activity - field_sharing: Sharing - text_line_separated: Multiple values allowed (one line for each value). - label_close_versions: Close completed versions - button_unarchive: Unarchive - label_start_to_end: start to end - label_start_to_start: start to start - field_issue_to: Related issue - default_issue_status_in_progress: In Progress - text_own_membership_delete_confirmation: |- - You are about to remove some or all of your permissions and may no longer be able to edit this project after that. - Are you sure you want to continue? - label_board_sticky: Sticky - label_board_locked: Locked - permission_export_wiki_pages: Export wiki pages - setting_cache_formatted_text: Cache formatted text - permission_manage_project_activities: Manage project activities - error_unable_delete_issue_status: Unable to delete issue status - label_profile: Profile - permission_manage_subtasks: Manage subtasks - field_parent_issue: Parent task - label_subtask_plural: Subtasks - label_project_copy_notifications: Send email notifications during the project copy - error_can_not_delete_custom_field: Unable to delete custom field - error_unable_to_connect: Unable to connect (%{value}) - error_can_not_remove_role: This role is in use and can not be deleted. - error_can_not_delete_tracker: This tracker contains issues and can't be deleted. - field_principal: Principal - label_my_page_block: My page block - notice_failed_to_save_members: "Failed to save member(s): %{errors}." - text_zoom_out: Zoom out - text_zoom_in: Zoom in - notice_unable_delete_time_entry: Unable to delete time log entry. - label_overall_spent_time: Overall spent time - field_time_entries: Log time - project_module_gantt: Gantt - project_module_calendar: Calendar - button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" - field_text: Text field - label_user_mail_option_only_owner: Only for things I am the owner of - setting_default_notification_option: Default notification option - label_user_mail_option_only_my_events: Only for things I watch or I'm involved in - label_user_mail_option_only_assigned: Only for things I am assigned to - label_user_mail_option_none: No events - field_member_of_group: Assignee's group - field_assigned_to_role: Assignee's role - notice_not_authorized_archived_project: The project you're trying to access has been archived. - label_principal_search: "Search for user or group:" - label_user_search: "Search for user:" - field_visible: Visible - setting_emails_header: Emails header - setting_commit_logtime_activity_id: Activity for logged time - text_time_logged_by_changeset: Applied in changeset %{value}. - setting_commit_logtime_enabled: Enable time logging - notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) - setting_gantt_items_limit: Maximum number of items displayed on the gantt chart - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: 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: Commit messages 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 - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - 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 predmet - one: 1 predmet - other: "%{count} predmeti" - 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.
    Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account - text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: Svi - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: With subprojects - label_cross_project_tree: With project tree - label_cross_project_hierarchy: With project hierarchy - label_cross_project_system: With all projects - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2b/2bf169c59e60650b88bcb73540687a2bc02b9736.svn-base --- a/.svn/pristine/2b/2bf169c59e60650b88bcb73540687a2bc02b9736.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 CalendarTest < ActiveSupport::TestCase - - def test_monthly - c = Redmine::Helpers::Calendar.new(Date.today, :fr, :month) - assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday] - - c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :month) - assert_equal ['2007-06-25'.to_date, '2007-08-05'.to_date], [c.startdt, c.enddt] - - c = Redmine::Helpers::Calendar.new(Date.today, :en, :month) - assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday] - end - - def test_weekly - c = Redmine::Helpers::Calendar.new(Date.today, :fr, :week) - assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday] - - c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :week) - assert_equal ['2007-07-09'.to_date, '2007-07-15'.to_date], [c.startdt, c.enddt] - - c = Redmine::Helpers::Calendar.new(Date.today, :en, :week) - assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday] - end - - def test_monthly_start_day - [1, 6, 7].each do |day| - with_settings :start_of_week => day do - c = Redmine::Helpers::Calendar.new(Date.today, :en, :month) - assert_equal day , c.startdt.cwday - assert_equal (day + 5) % 7 + 1, c.enddt.cwday - end - end - end - - def test_weekly_start_day - [1, 6, 7].each do |day| - with_settings :start_of_week => day do - c = Redmine::Helpers::Calendar.new(Date.today, :en, :week) - assert_equal day, c.startdt.cwday - assert_equal (day + 5) % 7 + 1, c.enddt.cwday - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2b/2bffeae9e506603f05893c49874a197ccc797b57.svn-base --- a/.svn/pristine/2b/2bffeae9e506603f05893c49874a197ccc797b57.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -

    <%= l(:label_board_plural) %>

    - - - - - - - - - -<% Board.board_tree(@boards) do |board, level| %> - - - - - - -<% end %> - -
    <%= l(:label_board) %><%= l(:label_topic_plural) %><%= l(:label_message_plural) %><%= l(:label_message_last) %>
    - <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "board" %>
    - <%=h board.description %> -
    <%= board.topics_count %><%= board.messages_count %> - <% if board.last_message %> - <%= authoring board.last_message.created_on, board.last_message.author %>
    - <%= link_to_message board.last_message %> - <% end %> -
    - -<% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_messages => 1, :key => User.current.rss_key} %> -<% end %> - -<% content_for :header_tags do %> - <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> -<% end %> - -<% html_title l(:label_board_plural) %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c0e2ce37084f59c8aba6396a829a848205403ce.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c0e2ce37084f59c8aba6396a829a848205403ce.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,134 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RoutingIssuesTest < ActionController::IntegrationTest + def test_issues_rest_actions + assert_routing( + { :method => 'get', :path => "/issues" }, + { :controller => 'issues', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/issues.pdf" }, + { :controller => 'issues', :action => 'index', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/issues.atom" }, + { :controller => 'issues', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues.xml" }, + { :controller => 'issues', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64" }, + { :controller => 'issues', :action => 'show', :id => '64' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.pdf" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.atom" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.xml" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/issues.xml" }, + { :controller => 'issues', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64/edit" }, + { :controller => 'issues', :action => 'edit', :id => '64' } + ) + assert_routing( + { :method => 'put', :path => "/issues/1.xml" }, + { :controller => 'issues', :action => 'update', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/1.xml" }, + { :controller => 'issues', :action => 'destroy', :id => '1', + :format => 'xml' } + ) + end + + def test_issues_rest_actions_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/23/issues" }, + { :controller => 'issues', :action => 'index', :project_id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.pdf" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.atom" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.xml" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/23/issues" }, + { :controller => 'issues', :action => 'create', :project_id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues/new" }, + { :controller => 'issues', :action => 'new', :project_id => '23' } + ) + end + + def test_issues_form_update + ["post", "put"].each do |method| + assert_routing( + { :method => method, :path => "/projects/23/issues/update_form" }, + { :controller => 'issues', :action => 'update_form', :project_id => '23' } + ) + end + end + + def test_issues_extra_actions + assert_routing( + { :method => 'get', :path => "/projects/23/issues/64/copy" }, + { :controller => 'issues', :action => 'new', :project_id => '23', + :copy_from => '64' } + ) + # For updating the bulk edit form + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/issues/bulk_edit" }, + { :controller => 'issues', :action => 'bulk_edit' } + ) + end + assert_routing( + { :method => 'post', :path => "/issues/bulk_update" }, + { :controller => 'issues', :action => 'bulk_update' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c2690417bf095320f6312e550c41841ed5d033a.svn-base --- a/.svn/pristine/2c/2c2690417bf095320f6312e550c41841ed5d033a.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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_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 d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c2981c9fdf45fa7fce93a5afc05807f4372fb62.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c2981c9fdf45fa7fce93a5afc05807f4372fb62.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,869 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 QueryColumn + attr_accessor :name, :sortable, :groupable, :default_order + include Redmine::I18n + + def initialize(name, options={}) + self.name = name + self.sortable = options[:sortable] + self.groupable = options[:groupable] || false + if groupable == true + self.groupable = name.to_s + end + self.default_order = options[:default_order] + @inline = options.key?(:inline) ? options[:inline] : true + @caption_key = options[:caption] || "field_#{name}".to_sym + @frozen = options[:frozen] + end + + def caption + @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key + end + + # Returns true if the column is sortable, otherwise false + def sortable? + !@sortable.nil? + end + + def sortable + @sortable.is_a?(Proc) ? @sortable.call : @sortable + end + + def inline? + @inline + end + + def frozen? + @frozen + end + + def value(object) + object.send name + end + + def css_classes + name + end +end + +class QueryCustomFieldColumn < QueryColumn + + def initialize(custom_field) + self.name = "cf_#{custom_field.id}".to_sym + self.sortable = custom_field.order_statement || false + self.groupable = custom_field.group_statement || false + @inline = true + @cf = custom_field + end + + def caption + @cf.name + end + + def custom_field + @cf + end + + def value(object) + if custom_field.visible_by?(object.project, User.current) + cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} + cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first + else + nil + end + end + + def css_classes + @css_classes ||= "#{name} #{@cf.field_format}" + end +end + +class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn + + def initialize(association, custom_field) + super(custom_field) + self.name = "#{association}.cf_#{custom_field.id}".to_sym + # TODO: support sorting/grouping by association custom field + self.sortable = false + self.groupable = false + @association = association + end + + def value(object) + if assoc = object.send(@association) + super(assoc) + end + end + + def css_classes + @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}" + end +end + +class Query < ActiveRecord::Base + class StatementInvalid < ::ActiveRecord::StatementInvalid + end + + VISIBILITY_PRIVATE = 0 + VISIBILITY_ROLES = 1 + VISIBILITY_PUBLIC = 2 + + belongs_to :project + belongs_to :user + has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id" + serialize :filters + serialize :column_names + serialize :sort_criteria, Array + serialize :options, Hash + + attr_protected :project_id, :user_id + + validates_presence_of :name + validates_length_of :name, :maximum => 255 + validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] } + validate :validate_query_filters + validate do |query| + errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank? + end + + after_save do |query| + if query.visibility_changed? && query.visibility != VISIBILITY_ROLES + query.roles.clear + end + end + + class_attribute :operators + self.operators = { + "=" => :label_equals, + "!" => :label_not_equals, + "o" => :label_open_issues, + "c" => :label_closed_issues, + "!*" => :label_none, + "*" => :label_any, + ">=" => :label_greater_or_equal, + "<=" => :label_less_or_equal, + "><" => :label_between, + " :label_in_less_than, + ">t+" => :label_in_more_than, + "> :label_in_the_next_days, + "t+" => :label_in, + "t" => :label_today, + "ld" => :label_yesterday, + "w" => :label_this_week, + "lw" => :label_last_week, + "l2w" => [:label_last_n_weeks, {:count => 2}], + "m" => :label_this_month, + "lm" => :label_last_month, + "y" => :label_this_year, + ">t-" => :label_less_than_ago, + " :label_more_than_ago, + "> :label_in_the_past_days, + "t-" => :label_ago, + "~" => :label_contains, + "!~" => :label_not_contains, + "=p" => :label_any_issues_in_project, + "=!p" => :label_any_issues_not_in_project, + "!p" => :label_no_issues_in_project + } + + class_attribute :operators_by_filter_type + self.operators_by_filter_type = { + :list => [ "=", "!" ], + :list_status => [ "o", "=", "!", "c", "*" ], + :list_optional => [ "=", "!", "!*", "*" ], + :list_subprojects => [ "*", "!*", "=" ], + :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "=", "~", "!", "!~", "!*", "*" ], + :text => [ "~", "!~", "!*", "*" ], + :integer => [ "=", ">=", "<=", "><", "!*", "*" ], + :float => [ "=", ">=", "<=", "><", "!*", "*" ], + :relation => ["=", "=p", "=!p", "!p", "!*", "*"] + } + + class_attribute :available_columns + self.available_columns = [] + + class_attribute :queried_class + + def queried_table_name + @queried_table_name ||= self.class.queried_class.table_name + end + + def initialize(attributes=nil, *args) + super attributes + @is_for_all = project.nil? + end + + # Builds the query from the given params + def build_from_params(params) + if params[:fields] || params[:f] + self.filters = {} + add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) + else + available_filters.keys.each do |field| + add_short_filter(field, params[field]) if params[field] + end + end + self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) + self.column_names = params[:c] || (params[:query] && params[:query][:column_names]) + self + end + + # Builds a new query from the given params and attributes + def self.build_from_params(params, attributes={}) + new(attributes).build_from_params(params) + end + + def validate_query_filters + filters.each_key do |field| + if values_for(field) + case type_for(field) + when :integer + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } + when :float + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } + when :date, :date_past + case operator_for(field) + when "=", ">=", "<=", "><" + add_filter_error(field, :invalid) if values_for(field).detect {|v| + v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?) + } + when ">t-", "t+", " 'activerecord.errors.messages') + errors.add(:base, m) + end + + def editable_by?(user) + return false unless user + # Admin can edit them all and regular users can edit their private queries + return true if user.admin? || (is_private? && self.user_id == user.id) + # Members can not edit public queries that are for all project (only admin is allowed to) + is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project) + end + + def trackers + @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers + end + + # Returns a hash of localized labels for all filter operators + def self.operators_labels + operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h} + end + + # Returns a representation of the available filters for JSON serialization + def available_filters_as_json + json = {} + available_filters.each do |field, options| + json[field] = options.slice(:type, :name, :values).stringify_keys + end + json + end + + def all_projects + @all_projects ||= Project.visible.all + end + + def all_projects_values + return @all_projects_values if @all_projects_values + + values = [] + Project.project_tree(all_projects) do |p, level| + prefix = (level > 0 ? ('--' * level + ' ') : '') + values << ["#{prefix}#{p.name}", p.id.to_s] + end + @all_projects_values = values + end + + # Adds available filters + def initialize_available_filters + # implemented by sub-classes + end + protected :initialize_available_filters + + # Adds an available filter + def add_available_filter(field, options) + @available_filters ||= ActiveSupport::OrderedHash.new + @available_filters[field] = options + @available_filters + end + + # Removes an available filter + def delete_available_filter(field) + if @available_filters + @available_filters.delete(field) + end + end + + # Return a hash of available filters + def available_filters + unless @available_filters + initialize_available_filters + @available_filters.each do |field, options| + options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) + end + end + @available_filters + end + + def add_filter(field, operator, values=nil) + # values must be an array + return unless values.nil? || values.is_a?(Array) + # check if field is defined as an available filter + if available_filters.has_key? field + filter_options = available_filters[field] + filters[field] = {:operator => operator, :values => (values || [''])} + end + end + + def add_short_filter(field, expression) + return unless expression && available_filters.has_key?(field) + field_type = available_filters[field][:type] + operators_by_filter_type[field_type].sort.reverse.detect do |operator| + next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/ + values = $1 + add_filter field, operator, values.present? ? values.split('|') : [''] + end || add_filter(field, '=', expression.split('|')) + end + + # Add multiple filters using +add_filter+ + def add_filters(fields, operators, values) + if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash)) + fields.each do |field| + add_filter(field, operators[field], values && values[field]) + end + end + end + + def has_filter?(field) + filters and filters[field] + end + + def type_for(field) + available_filters[field][:type] if available_filters.has_key?(field) + end + + def operator_for(field) + has_filter?(field) ? filters[field][:operator] : nil + end + + def values_for(field) + has_filter?(field) ? filters[field][:values] : nil + end + + def value_for(field, index=0) + (values_for(field) || [])[index] + end + + def label_for(field) + label = available_filters[field][:name] if available_filters.has_key?(field) + label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) + end + + def self.add_available_column(column) + self.available_columns << (column) if column.is_a?(QueryColumn) + end + + # Returns an array of columns that can be used to group the results + def groupable_columns + available_columns.select {|c| c.groupable} + end + + # Returns a Hash of columns and the key for sorting + def sortable_columns + available_columns.inject({}) {|h, column| + h[column.name.to_s] = column.sortable + h + } + end + + def columns + # preserve the column_names order + cols = (has_default_columns? ? default_columns_names : column_names).collect do |name| + available_columns.find { |col| col.name == name } + end.compact + available_columns.select(&:frozen?) | cols + end + + def inline_columns + columns.select(&:inline?) + end + + def block_columns + columns.reject(&:inline?) + end + + def available_inline_columns + available_columns.select(&:inline?) + end + + def available_block_columns + available_columns.reject(&:inline?) + end + + def default_columns_names + [] + end + + def column_names=(names) + if names + names = names.select {|n| n.is_a?(Symbol) || !n.blank? } + names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } + # Set column_names to nil if default columns + if names == default_columns_names + names = nil + end + end + write_attribute(:column_names, names) + end + + def has_column?(column) + column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) + end + + def has_custom_field_column? + columns.any? {|column| column.is_a? QueryCustomFieldColumn} + end + + def has_default_columns? + column_names.nil? || column_names.empty? + end + + def sort_criteria=(arg) + c = [] + if arg.is_a?(Hash) + arg = arg.keys.sort.collect {|k| arg[k]} + end + c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']} + write_attribute(:sort_criteria, c) + end + + def sort_criteria + read_attribute(:sort_criteria) || [] + end + + def sort_criteria_key(arg) + sort_criteria && sort_criteria[arg] && sort_criteria[arg].first + end + + def sort_criteria_order(arg) + sort_criteria && sort_criteria[arg] && sort_criteria[arg].last + end + + def sort_criteria_order_for(key) + sort_criteria.detect {|k, order| key.to_s == k}.try(:last) + end + + # Returns the SQL sort order that should be prepended for grouping + def group_by_sort_order + if grouped? && (column = group_by_column) + order = sort_criteria_order_for(column.name) || column.default_order + column.sortable.is_a?(Array) ? + column.sortable.collect {|s| "#{s} #{order}"}.join(',') : + "#{column.sortable} #{order}" + end + end + + # Returns true if the query is a grouped query + def grouped? + !group_by_column.nil? + end + + def group_by_column + groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} + end + + def group_by_statement + group_by_column.try(:groupable) + end + + def project_statement + project_clauses = [] + if project && !project.descendants.active.empty? + ids = [project.id] + if has_filter?("subproject_id") + case operator_for("subproject_id") + when '=' + # include the selected subprojects + ids += values_for("subproject_id").each(&:to_i) + when '!*' + # main project only + else + # all subprojects + ids += project.descendants.collect(&:id) + end + elsif Setting.display_subprojects_issues? + ids += project.descendants.collect(&:id) + end + project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') + elsif project + project_clauses << "#{Project.table_name}.id = %d" % project.id + end + project_clauses.any? ? project_clauses.join(' AND ') : nil + end + + def statement + # filters clauses + filters_clauses = [] + filters.each_key do |field| + next if field == "subproject_id" + v = values_for(field).clone + next unless v and !v.empty? + operator = operator_for(field) + + # "me" value subsitution + if %w(assigned_to_id author_id user_id watcher_id).include?(field) + if v.delete("me") + if User.current.logged? + v.push(User.current.id.to_s) + v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' + else + v.push("0") + end + end + end + + if field == 'project_id' + if v.delete('mine') + v += User.current.memberships.map(&:project_id).map(&:to_s) + end + end + + if field =~ /cf_(\d+)$/ + # custom field + filters_clauses << sql_for_custom_field(field, operator, v, $1) + elsif respond_to?("sql_for_#{field}_field") + # specific statement + filters_clauses << send("sql_for_#{field}_field", field, operator, v) + else + # regular field + filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')' + end + end if filters and valid? + + if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn) + # Excludes results for which the grouped custom field is not visible + filters_clauses << c.custom_field.visibility_by_project_condition + end + + filters_clauses << project_statement + filters_clauses.reject!(&:blank?) + + filters_clauses.any? ? filters_clauses.join(' AND ') : nil + end + + private + + def sql_for_custom_field(field, operator, value, custom_field_id) + db_table = CustomValue.table_name + db_field = 'value' + filter = @available_filters[field] + return nil unless filter + if filter[:field].format.target_class && filter[:field].format.target_class <= User + if value.delete('me') + value.push User.current.id.to_s + end + end + not_in = nil + if operator == '!' + # Makes ! operator work for custom fields with multiple values + operator = '=' + not_in = 'NOT' + end + customized_key = "id" + customized_class = queried_class + if field =~ /^(.+)\.cf_/ + assoc = $1 + customized_key = "#{assoc}_id" + customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil + raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class + end + where = sql_for_field(field, operator, value, db_table, db_field, true) + if operator =~ /[<>]/ + where = "(#{where}) AND #{db_table}.#{db_field} <> ''" + end + "#{queried_table_name}.#{customized_key} #{not_in} IN (" + + "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" + + " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" + + " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))" + end + + # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ + def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) + sql = '' + case operator + when "=" + if value.any? + case type_for(field) + when :date, :date_past + sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first)) + when :integer + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})" + else + sql = "#{db_table}.#{db_field} = #{value.first.to_i}" + end + when :float + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})" + else + sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" + end + else + sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + end + else + # IN an empty set + sql = "1=0" + end + when "!" + if value.any? + sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" + else + # NOT IN an empty set + sql = "1=1" + end + when "!*" + sql = "#{db_table}.#{db_field} IS NULL" + sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter + when "*" + sql = "#{db_table}.#{db_field} IS NOT NULL" + sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter + when ">=" + if [:date, :date_past].include?(type_for(field)) + sql = date_clause(db_table, db_field, parse_date(value.first), nil) + else + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})" + else + sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" + end + end + when "<=" + if [:date, :date_past].include?(type_for(field)) + sql = date_clause(db_table, db_field, nil, parse_date(value.first)) + else + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})" + else + sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" + end + end + when "><" + if [:date, :date_past].include?(type_for(field)) + sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1])) + else + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})" + else + sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" + end + end + when "o" + sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id" + when "c" + sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id" + when ">t-" + # >= today - n days + sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil) + when "t+" + # >= today + n days + sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) + when "= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) + when "lw" + # = last week + first_day_of_week = l(:general_first_day_of_week).to_i + day_of_week = Date.today.cwday + days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1) + when "l2w" + # = last 2 weeks + first_day_of_week = l(:general_first_day_of_week).to_i + day_of_week = Date.today.cwday + days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1) + when "m" + # = this month + date = Date.today + sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month) + when "lm" + # = last month + date = Date.today.prev_month + sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month) + when "y" + # = this year + date = Date.today + sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year) + when "~" + sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" + when "!~" + sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" + else + raise "Unknown query operator #{operator}" + end + + return sql + end + + # Adds a filter for the given custom field + def add_custom_field_filter(field, assoc=nil) + options = field.format.query_filter_options(field, self) + if field.format.target_class && field.format.target_class <= User + if options[:values].is_a?(Array) && User.current.logged? + options[:values].unshift ["<< #{l(:label_me)} >>", "me"] + end + end + + filter_id = "cf_#{field.id}" + filter_name = field.name + if assoc.present? + filter_id = "#{assoc}.#{filter_id}" + filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) + end + add_available_filter filter_id, options.merge({ + :name => filter_name, + :field => field + }) + end + + # Adds filters for the given custom fields scope + def add_custom_fields_filters(scope, assoc=nil) + scope.visible.where(:is_filter => true).sorted.each do |field| + add_custom_field_filter(field, assoc) + end + end + + # Adds filters for the given associations custom fields + def add_associations_custom_fields_filters(*associations) + fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class) + associations.each do |assoc| + association_klass = queried_class.reflect_on_association(assoc).klass + fields_by_class.each do |field_class, fields| + if field_class.customized_class <= association_klass + fields.sort.each do |field| + add_custom_field_filter(field, assoc) + end + end + end + end + end + + # Returns a SQL clause for a date or datetime field. + def date_clause(table, field, from, to) + s = [] + if from + if from.is_a?(Date) + from = Time.local(from.year, from.month, from.day).yesterday.end_of_day + else + from = from - 1 # second + end + if self.class.default_timezone == :utc + from = from.utc + end + s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from)]) + end + if to + if to.is_a?(Date) + to = Time.local(to.year, to.month, to.day).end_of_day + end + if self.class.default_timezone == :utc + to = to.utc + end + s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)]) + end + s.join(' AND ') + end + + # Returns a SQL clause for a date or datetime field using relative dates. + def relative_date_clause(table, field, days_from, days_to) + date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) + end + + # Returns a Date or Time from the given filter value + def parse_date(arg) + if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/ + Time.parse(arg) rescue nil + else + Date.parse(arg) rescue nil + end + end + + # Additional joins required for the given sort options + def joins_for_order_statement(order_options) + joins = [] + + if order_options + if order_options.include?('authors') + joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id" + end + order_options.scan(/cf_\d+/).uniq.each do |name| + column = available_columns.detect {|c| c.name.to_s == name} + join = column && column.custom_field.join_for_order_statement + if join + joins << join + end + end + end + + joins.any? ? joins.join(' ') : nil + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c3a4de5492d19130df8d9d9f0bc6d32734619b7.svn-base --- a/.svn/pristine/2c/2c3a4de5492d19130df8d9d9f0bc6d32734619b7.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,341 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'redmine/scm/adapters/abstract_adapter' -require 'cgi' - -module Redmine - module Scm - module Adapters - class MercurialAdapter < AbstractAdapter - - # Mercurial executable name - HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg" - HELPERS_DIR = File.dirname(__FILE__) + "/mercurial" - HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py" - TEMPLATE_NAME = "hg-template" - TEMPLATE_EXTENSION = "tmpl" - - # raised if hg command exited with error, e.g. unknown revision. - class HgCommandAborted < CommandFailed; end - - class << self - def client_command - @@bin ||= HG_BIN - end - - def sq_bin - @@sq_bin ||= shell_quote_command - end - - def client_version - @@client_version ||= (hgversion || []) - end - - def client_available - client_version_above?([1, 2]) - end - - def hgversion - # The hg version is expressed either as a - # release number (eg 0.9.5 or 1.0) or as a revision - # id composed of 12 hexa characters. - theversion = hgversion_from_command_line.dup - if theversion.respond_to?(:force_encoding) - theversion.force_encoding('ASCII-8BIT') - end - if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)}) - m[2].scan(%r{\d+}).collect(&:to_i) - end - end - - def hgversion_from_command_line - shellout("#{sq_bin} --version") { |io| io.read }.to_s - end - - def template_path - @@template_path ||= template_path_for(client_version) - end - - def template_path_for(version) - "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}" - end - end - - def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) - super - @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding - end - - def path_encoding - @path_encoding - end - - def info - tip = summary['repository']['tip'] - Info.new(:root_url => CGI.unescape(summary['repository']['root']), - :lastrev => Revision.new(:revision => tip['revision'], - :scmid => tip['node'])) - # rescue HgCommandAborted - rescue Exception => e - logger.error "hg: error during getting info: #{e.message}" - nil - end - - def tags - as_ary(summary['repository']['tag']).map { |e| e['name'] } - end - - # Returns map of {'tag' => 'nodeid', ...} - def tagmap - alist = as_ary(summary['repository']['tag']).map do |e| - e.values_at('name', 'node') - end - Hash[*alist.flatten] - end - - def branches - brs = [] - as_ary(summary['repository']['branch']).each do |e| - br = Branch.new(e['name']) - br.revision = e['revision'] - br.scmid = e['node'] - brs << br - end - brs - end - - # Returns map of {'branch' => 'nodeid', ...} - def branchmap - alist = as_ary(summary['repository']['branch']).map do |e| - e.values_at('name', 'node') - end - Hash[*alist.flatten] - end - - def summary - return @summary if @summary - hg 'rhsummary' do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - @summary = parse_xml(output)['rhsummary'] - rescue - end - end - end - private :summary - - def entries(path=nil, identifier=nil, options={}) - p1 = scm_iconv(@path_encoding, 'UTF-8', path) - manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)), - CGI.escape(without_leading_slash(p1.to_s))) do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - parse_xml(output)['rhmanifest']['repository']['manifest'] - rescue - end - end - path_prefix = path.blank? ? '' : with_trailling_slash(path) - - entries = Entries.new - as_ary(manifest['dir']).each do |e| - n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) - p = "#{path_prefix}#{n}" - entries << Entry.new(:name => n, :path => p, :kind => 'dir') - end - - as_ary(manifest['file']).each do |e| - n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) - p = "#{path_prefix}#{n}" - lr = Revision.new(:revision => e['revision'], :scmid => e['node'], - :identifier => e['node'], - :time => Time.at(e['time'].to_i)) - entries << Entry.new(:name => n, :path => p, :kind => 'file', - :size => e['size'].to_i, :lastrev => lr) - end - - entries - rescue HgCommandAborted - nil # means not found - end - - def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) - revs = Revisions.new - each_revision(path, identifier_from, identifier_to, options) { |e| revs << e } - revs - end - - # Iterates the revisions by using a template file that - # makes Mercurial produce a xml output. - def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) - hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] - hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" - hg_args << '--limit' << options[:limit] if options[:limit] - hg_args << hgtarget(path) unless path.blank? - log = hg(*hg_args) do |io| - output = io.read - if output.respond_to?(:force_encoding) - output.force_encoding('UTF-8') - end - begin - # Mercurial < 1.5 does not support footer template for '' - parse_xml("#{output}")['log'] - rescue - end - end - as_ary(log['logentry']).each do |le| - cpalist = as_ary(le['paths']['path-copied']).map do |e| - [e['__content__'], e['copyfrom-path']].map do |s| - scm_iconv('UTF-8', @path_encoding, CGI.unescape(s)) - end - end - cpmap = Hash[*cpalist.flatten] - paths = as_ary(le['paths']['path']).map do |e| - p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) ) - {:action => e['action'], - :path => with_leading_slash(p), - :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil), - :from_revision => (cpmap.member?(p) ? le['node'] : nil)} - end.sort { |a, b| a[:path] <=> b[:path] } - parents_ary = [] - as_ary(le['parents']['parent']).map do |par| - parents_ary << par['__content__'] if par['__content__'] != "000000000000" - end - yield Revision.new(:revision => le['revision'], - :scmid => le['node'], - :author => (le['author']['__content__'] rescue ''), - :time => Time.parse(le['date']['__content__']), - :message => le['msg']['__content__'], - :paths => paths, - :parents => parents_ary) - end - self - end - - # Returns list of nodes in the specified branch - def nodes_in_branch(branch, options={}) - hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)] - hg_args << '--from' << CGI.escape(branch) - hg_args << '--to' << '0' - hg_args << '--limit' << options[:limit] if options[:limit] - hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } - end - - def diff(path, identifier_from, identifier_to=nil) - hg_args = %w|rhdiff| - if identifier_to - hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) - else - hg_args << '-c' << hgrev(identifier_from) - end - unless path.blank? - p = scm_iconv(@path_encoding, 'UTF-8', path) - hg_args << CGI.escape(hgtarget(p)) - end - diff = [] - hg *hg_args do |io| - io.each_line do |line| - diff << line - end - end - diff - rescue HgCommandAborted - nil # means not found - end - - def cat(path, identifier=nil) - p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) - hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| - io.binmode - io.read - end - rescue HgCommandAborted - nil # means not found - end - - def annotate(path, identifier=nil) - p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) - blame = Annotate.new - hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| - io.each_line do |line| - line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding) - next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$} - r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, - :identifier => $3) - blame.add_line($4.rstrip, r) - end - end - blame - rescue HgCommandAborted - # means not found or cannot be annotated - Annotate.new - end - - class Revision < Redmine::Scm::Adapters::Revision - # Returns the readable identifier - def format_identifier - "#{revision}:#{scmid}" - end - end - - # Runs 'hg' command with the given args - def hg(*args, &block) - repo_path = root_url || url - full_args = ['-R', repo_path, '--encoding', 'utf-8'] - full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" - full_args << '--config' << 'diff.git=false' - full_args += args - ret = shellout( - self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '), - &block - ) - if $? && $?.exitstatus != 0 - raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" - end - ret - end - private :hg - - # Returns correct revision identifier - def hgrev(identifier, sq=false) - rev = identifier.blank? ? 'tip' : identifier.to_s - rev = shell_quote(rev) if sq - rev - end - private :hgrev - - def hgtarget(path) - path ||= '' - root_url + '/' + without_leading_slash(path) - end - private :hgtarget - - def as_ary(o) - return [] unless o - o.is_a?(Array) ? o : Array[o] - end - private :as_ary - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c4595b19c4d38826042063686f09c958ada056c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c4595b19c4d38826042063686f09c958ada056c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 DocumentCategoryCustomField < CustomField + def type_name + :enumeration_doc_categories + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c5433d8c76b122107da1fadd0241a5eef71ece6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c5433d8c76b122107da1fadd0241a5eef71ece6.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RoutingEnumerationsTest < ActionController::IntegrationTest + def test_enumerations + assert_routing( + { :method => 'get', :path => "/enumerations" }, + { :controller => 'enumerations', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/enumerations/new" }, + { :controller => 'enumerations', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/enumerations" }, + { :controller => 'enumerations', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/enumerations/2/edit" }, + { :controller => 'enumerations', :action => 'edit', :id => '2' } + ) + assert_routing( + { :method => 'put', :path => "/enumerations/2" }, + { :controller => 'enumerations', :action => 'update', :id => '2' } + ) + assert_routing( + { :method => 'delete', :path => "/enumerations/2" }, + { :controller => 'enumerations', :action => 'destroy', :id => '2' } + ) + assert_routing( + { :method => 'get', :path => "/enumerations/issue_priorities.xml" }, + { :controller => 'enumerations', :action => 'index', :type => 'issue_priorities', :format => 'xml' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c76b08002398b97784edeca2011d8a77c453847.svn-base --- a/.svn/pristine/2c/2c76b08002398b97784edeca2011d8a77c453847.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -

    <%=l(:label_news_latest)%>

    - -<%= render(:partial => 'news/news', - :collection => News.find(:all, - :limit => 10, - :order => "#{News.table_name}.created_on DESC", - :conditions => "#{News.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", - :include => [:project, :author])) unless @user.projects.empty? %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2c94248d7eb61aa48f3ae6ffba79f9dd6a0b9346.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c94248d7eb61aa48f3ae6ffba79f9dd6a0b9346.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::Utils::DateCalculationTest < ActiveSupport::TestCase + include Redmine::Utils::DateCalculation + + def test_working_days_without_non_working_week_days + with_settings :non_working_week_days => [] do + assert_working_days 18, '2012-10-09', '2012-10-27' + assert_working_days 6, '2012-10-09', '2012-10-15' + assert_working_days 5, '2012-10-09', '2012-10-14' + assert_working_days 3, '2012-10-09', '2012-10-12' + assert_working_days 3, '2012-10-14', '2012-10-17' + assert_working_days 16, '2012-10-14', '2012-10-30' + end + end + + def test_working_days_with_non_working_week_days + with_settings :non_working_week_days => %w(6 7) do + assert_working_days 14, '2012-10-09', '2012-10-27' + assert_working_days 4, '2012-10-09', '2012-10-15' + assert_working_days 4, '2012-10-09', '2012-10-14' + assert_working_days 3, '2012-10-09', '2012-10-12' + assert_working_days 8, '2012-10-09', '2012-10-19' + assert_working_days 8, '2012-10-11', '2012-10-23' + assert_working_days 2, '2012-10-14', '2012-10-17' + assert_working_days 11, '2012-10-14', '2012-10-30' + end + end + + def test_add_working_days_without_non_working_week_days + with_settings :non_working_week_days => [] do + assert_add_working_days '2012-10-10', '2012-10-10', 0 + assert_add_working_days '2012-10-11', '2012-10-10', 1 + assert_add_working_days '2012-10-12', '2012-10-10', 2 + assert_add_working_days '2012-10-13', '2012-10-10', 3 + assert_add_working_days '2012-10-25', '2012-10-10', 15 + end + end + + def test_add_working_days_with_non_working_week_days + with_settings :non_working_week_days => %w(6 7) do + assert_add_working_days '2012-10-10', '2012-10-10', 0 + assert_add_working_days '2012-10-11', '2012-10-10', 1 + assert_add_working_days '2012-10-12', '2012-10-10', 2 + assert_add_working_days '2012-10-15', '2012-10-10', 3 + assert_add_working_days '2012-10-31', '2012-10-10', 15 + assert_add_working_days '2012-10-19', '2012-10-09', 8 + assert_add_working_days '2012-10-23', '2012-10-11', 8 + end + end + + def assert_working_days(expected_days, from, to) + assert_equal expected_days, working_days(from.to_date, to.to_date) + end + + def assert_add_working_days(expected_date, from, working_days) + assert_equal expected_date.to_date, add_working_days(from.to_date, working_days) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2c/2caf818a4b5c04288552a4ab14703d73f1006735.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2caf818a4b5c04288552a4ab14703d73f1006735.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,217 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 ProjectEnumerationsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :custom_fields, :custom_fields_projects, + :custom_fields_trackers, :custom_values, + :time_entries + + self.use_transactional_fixtures = false + + def setup + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_update_to_override_system_activities + @request.session[:user_id] = 2 # manager + billable_field = TimeEntryActivityCustomField.find_by_name("Billable") + + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate + "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value + "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value + "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes + } + + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + # Created project specific activities... + project = Project.find('ecookbook') + + # ... Design + design = project.time_entry_activities.find_by_name("Design") + assert design, "Project activity not found" + + assert_equal 9, design.parent_id # Relate to the system activity + assert_not_equal design.parent.id, design.id # Different records + assert_equal design.parent.name, design.name # Same name + assert !design.active? + + # ... Development + development = project.time_entry_activities.find_by_name("Development") + assert development, "Project activity not found" + + assert_equal 10, development.parent_id # Relate to the system activity + assert_not_equal development.parent.id, development.id # Different records + assert_equal development.parent.name, development.name # Same name + assert development.active? + assert_equal "0", development.custom_value_for(billable_field).value + + # ... Inactive Activity + previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity") + assert previously_inactive, "Project activity not found" + + assert_equal 14, previously_inactive.parent_id # Relate to the system activity + assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records + assert_equal previously_inactive.parent.name, previously_inactive.name # Same name + assert previously_inactive.active? + assert_equal "1", previously_inactive.custom_value_for(billable_field).value + + # ... QA + assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified" + end + + def test_update_will_update_project_specific_activities + @request.session[:user_id] = 2 # manager + + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific', + :parent => TimeEntryActivity.first, + :project => Project.find(1), + :active => true + }) + assert project_activity.save + project_activity_two = TimeEntryActivity.new({ + :name => 'Project Specific Two', + :parent => TimeEntryActivity.last, + :project => Project.find(1), + :active => true + }) + assert project_activity_two.save + + + put :update, :project_id => 1, :enumerations => { + project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate + project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate + } + + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + # Created project specific activities... + project = Project.find('ecookbook') + assert_equal 2, project.time_entry_activities.count + + activity_one = project.time_entry_activities.find_by_name(project_activity.name) + assert activity_one, "Project activity not found" + assert_equal project_activity.id, activity_one.id + assert !activity_one.active? + + activity_two = project.time_entry_activities.find_by_name(project_activity_two.name) + assert activity_two, "Project activity not found" + assert_equal project_activity_two.id, activity_two.id + assert !activity_two.active? + end + + def test_update_when_creating_new_activities_will_convert_existing_data + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size + + @request.session[:user_id] = 2 # manager + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate + } + assert_response :redirect + + # No more TimeEntries using the system activity + assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities" + # All TimeEntries using project activity + project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1) + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity" + end + + def test_update_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised + # TODO: Need to cause an exception on create but these tests + # aren't setup for mocking. Just create a record now so the + # second one is a dupicate + parent = TimeEntryActivity.find(9) + TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true}) + TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'}) + + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size + assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size + + @request.session[:user_id] = 2 # manager + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design + "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value + } + assert_response :redirect + + # TimeEntries shouldn't have been reassigned on the failed record + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities" + # TimeEntries shouldn't have been reassigned on the saved record either + assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities" + end + + def test_destroy + @request.session[:user_id] = 2 # manager + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific', + :parent => TimeEntryActivity.first, + :project => Project.find(1), + :active => true + }) + assert project_activity.save + project_activity_two = TimeEntryActivity.new({ + :name => 'Project Specific Two', + :parent => TimeEntryActivity.last, + :project => Project.find(1), + :active => true + }) + assert project_activity_two.save + + delete :destroy, :project_id => 1 + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + assert_nil TimeEntryActivity.find_by_id(project_activity.id) + assert_nil TimeEntryActivity.find_by_id(project_activity_two.id) + end + + def test_destroy_should_reassign_time_entries_back_to_the_system_activity + @request.session[:user_id] = 2 # manager + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific Design', + :parent => TimeEntryActivity.find(9), + :project => Project.find(1), + :active => true + }) + assert project_activity.save + assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9]) + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size + + delete :destroy, :project_id => 1 + assert_response :redirect + assert_redirected_to '/projects/ecookbook/settings/activities' + + assert_nil TimeEntryActivity.find_by_id(project_activity.id) + assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity" + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity" + end + +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d1b6f7772fdd853c88983e5f12b084f72e3159d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d1b6f7772fdd853c88983e5f12b084f72e3159d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,13 @@ +class AddCommitAccessPermission < ActiveRecord::Migration + def self.up + Role.all.select { |r| not r.builtin? }.each do |r| + r.add_permission!(:commit_access) + end + end + + def self.down + Role.all.select { |r| not r.builtin? }.each do |r| + r.remove_permission!(:commit_access) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d32672cd69f23850e3e5406acee5c4056b23479.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d32672cd69f23850e3e5406acee5c4056b23479.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 SettingsController < ApplicationController + layout 'admin' + menu_item :plugins, :only => :plugin + + helper :queries + + before_filter :require_admin + + def index + edit + render :action => 'edit' + end + + def edit + @notifiables = Redmine::Notifiable.all + if request.post? && params[:settings] && params[:settings].is_a?(Hash) + settings = (params[:settings] || {}).dup.symbolize_keys + settings.each do |name, value| + Setting.set_from_params name, value + end + flash[:notice] = l(:notice_successful_update) + redirect_to settings_path(:tab => params[:tab]) + else + @options = {} + user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]} + @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]} + @deliveries = ActionMailer::Base.perform_deliveries + + @guessed_host_and_path = request.host_with_port.dup + @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? + + @commit_update_keywords = Setting.commit_update_keywords.dup + @commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any? + + Redmine::Themes.rescan + end + end + + def plugin + @plugin = Redmine::Plugin.find(params[:id]) + unless @plugin.configurable? + render_404 + return + end + + if request.post? + Setting.send "plugin_#{@plugin.id}=", params[:settings] + flash[:notice] = l(:notice_successful_update) + redirect_to plugin_settings_path(@plugin) + else + @partial = @plugin.settings[:partial] + @settings = Setting.send "plugin_#{@plugin.id}" + end + rescue Redmine::PluginNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d40a9e2c317c7ef78f729e12dcba72428f142c7.svn-base --- a/.svn/pristine/2d/2d40a9e2c317c7ef78f729e12dcba72428f142c7.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1083 +0,0 @@ -sl: - # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) - direction: ltr - 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: [Nedelja, Ponedeljek, Torek, Sreda, ÄŒetrtek, Petek, Sobota] - abbr_day_names: [Ned, Pon, To, Sr, ÄŒet, Pet, Sob] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, Januar, Februar, Marec, April, Maj, Junij, Julij, Avgust, September, Oktober, November, December] - abbr_month_names: [~, Jan, Feb, Mar, Apr, Maj, Jun, Jul, Aug, Sep, Okt, Nov, Dec] - # Used in date_select and datime_select. - order: - - :year - - :month - - :day - - time: - formats: - default: "%a, %d %b %Y %H:%M:%S %z" - time: "%H:%M" - short: "%d %b %H:%M" - long: "%B %d, %Y %H:%M" - am: "am" - pm: "pm" - - datetime: - distance_in_words: - half_a_minute: "pol minute" - less_than_x_seconds: - one: "manj kot 1. sekundo" - other: "manj kot %{count} sekund" - x_seconds: - one: "1. sekunda" - other: "%{count} sekund" - less_than_x_minutes: - one: "manj kot minuto" - other: "manj kot %{count} minut" - x_minutes: - one: "1 minuta" - other: "%{count} minut" - about_x_hours: - one: "okrog 1. ure" - other: "okrog %{count} ur" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "1 dan" - other: "%{count} dni" - about_x_months: - one: "okrog 1. mesec" - other: "okrog %{count} mesecev" - x_months: - one: "1 mesec" - other: "%{count} mesecev" - about_x_years: - one: "okrog 1. leto" - other: "okrog %{count} let" - over_x_years: - one: "veÄ kot 1. leto" - other: "veÄ kot %{count} let" - almost_x_years: - one: "skoraj 1. leto" - other: "skoraj %{count} let" - - number: - format: - separator: "," - delimiter: "." - precision: 3 - human: - format: - precision: 3 - delimiter: "" - storage_units: - format: "%n %u" - units: - kb: KB - tb: TB - gb: GB - byte: - one: Byte - other: Bytes - mb: MB - -# Used in array.to_sentence. - support: - array: - sentence_connector: "in" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "1. napaka je prepreÄila temu %{model} da bi se shranil" - other: "%{count} napak je prepreÄilo temu %{model} da bi se shranil" - messages: - inclusion: "ni vkljuÄen na seznamu" - exclusion: "je rezerviran" - invalid: "je napaÄen" - confirmation: "ne ustreza potrdilu" - accepted: "mora biti sprejet" - empty: "ne sme biti prazen" - blank: "ne sme biti neizpolnjen" - too_long: "je predolg" - too_short: "je prekratek" - wrong_length: "je napaÄne dolžine" - taken: "je že zaseden" - not_a_number: "ni Å¡tevilo" - not_a_date: "ni veljaven datum" - greater_than: "mora biti veÄji kot %{count}" - greater_than_or_equal_to: "mora biti veÄji ali enak kot %{count}" - equal_to: "mora biti enak kot %{count}" - less_than: "mora biti manjÅ¡i kot %{count}" - less_than_or_equal_to: "mora biti manjÅ¡i ali enak kot %{count}" - odd: "mora biti sodo" - even: "mora biti liho" - greater_than_start_date: "mora biti kasnejÅ¡i kot zaÄetni datum" - not_same_project: "ne pripada istemu projektu" - circular_dependency: "Ta odnos bi povzroÄil krožno odvisnost" - cant_link_an_issue_with_a_descendant: "Zahtevek ne more biti povezan s svojo podnalogo" - - actionview_instancetag_blank_option: Prosimo izberite - - general_text_No: 'Ne' - general_text_Yes: 'Da' - general_text_no: 'ne' - general_text_yes: 'da' - general_lang_name: 'SlovenÅ¡Äina' - general_csv_separator: ',' - general_csv_decimal_separator: '.' - general_csv_encoding: UTF-8 - general_pdf_encoding: UTF-8 - general_first_day_of_week: '1' - - notice_account_updated: RaÄun je bil uspeÅ¡no posodobljen. - notice_account_invalid_creditentials: NapaÄno uporabniÅ¡ko ime ali geslo - notice_account_password_updated: Geslo je bilo uspeÅ¡no posodobljeno. - notice_account_wrong_password: NapaÄno geslo - notice_account_register_done: RaÄun je bil uspeÅ¡no ustvarjen. Za aktivacijo potrdite povezavo, ki vam je bila poslana v e-nabiralnik. - notice_account_unknown_email: Neznan uporabnik. - notice_can_t_change_password: Ta raÄun za overovljanje uporablja zunanji. Gesla ni mogoÄe spremeniti. - notice_account_lost_email_sent: Poslano vam je bilo e-pismo z navodili za izbiro novega gesla. - notice_account_activated: VaÅ¡ raÄun je bil aktiviran. Sedaj se lahko prijavite. - notice_successful_create: Ustvarjanje uspelo. - notice_successful_update: Posodobitev uspela. - notice_successful_delete: Izbris uspel. - notice_successful_connection: Povezava uspela. - notice_file_not_found: Stran na katero se želite povezati ne obstaja ali pa je bila umaknjena. - notice_locking_conflict: Drug uporabnik je posodobil podatke. - notice_not_authorized: Nimate privilegijev za dostop do te strani. - notice_email_sent: "E-poÅ¡tno sporoÄilo je bilo poslano %{value}" - notice_email_error: "Ob poÅ¡iljanju e-sporoÄila je priÅ¡lo do napake (%{value})" - notice_feeds_access_key_reseted: VaÅ¡ RSS dostopni kljuÄ je bil ponastavljen. - notice_failed_to_save_issues: "Neuspelo shranjevanje %{count} zahtevka na %{total} izbranem: %{ids}." - notice_no_issue_selected: "Izbran ni noben zahtevek! Prosimo preverite zahtevke, ki jih želite urediti." - notice_account_pending: "VaÅ¡ raÄun je bil ustvarjen in Äaka na potrditev s strani administratorja." - notice_default_data_loaded: Privzete nastavitve so bile uspeÅ¡no naložene. - notice_unable_delete_version: Verzije ni bilo mogoÄe izbrisati. - - error_can_t_load_default_data: "Privzetih nastavitev ni bilo mogoÄe naložiti: %{value}" - error_scm_not_found: "Vnos ali revizija v shrambi ni bila najdena ." - error_scm_command_failed: "Med vzpostavljem povezave s shrambo je priÅ¡lo do napake: %{value}" - error_scm_annotate: "Vnos ne obstaja ali pa ga ni mogoÄe komentirati." - error_issue_not_found_in_project: 'Zahtevek ni bil najden ali pa ne pripada temu projektu' - - mail_subject_lost_password: "VaÅ¡e %{value} geslo" - mail_body_lost_password: 'Za spremembo glesla kliknite na naslednjo povezavo:' - mail_subject_register: "Aktivacija %{value} vaÅ¡ega raÄuna" - mail_body_register: 'Za aktivacijo vaÅ¡ega raÄuna kliknite na naslednjo povezavo:' - mail_body_account_information_external: "Za prijavo lahko uporabite vaÅ¡ %{value} raÄun." - mail_body_account_information: Informacije o vaÅ¡em raÄunu - mail_subject_account_activation_request: "%{value} zahtevek za aktivacijo raÄuna" - mail_body_account_activation_request: "Registriral se je nov uporabnik (%{value}). RaÄun Äaka na vaÅ¡o odobritev:" - mail_subject_reminder: "%{count} zahtevek(zahtevki) zapadejo v naslednjih %{days} dneh" - mail_body_reminder: "%{count} zahtevek(zahtevki), ki so vam dodeljeni bodo zapadli v naslednjih %{days} dneh:" - - gui_validation_error: 1 napaka - gui_validation_error_plural: "%{count} napak" - - field_name: Ime - field_description: Opis - field_summary: Povzetek - field_is_required: Zahtevano - field_firstname: Ime - field_lastname: Priimek - field_mail: E-naslov - field_filename: Datoteka - field_filesize: Velikost - field_downloads: Prenosi - field_author: Avtor - field_created_on: Ustvarjen - field_updated_on: Posodobljeno - field_field_format: Format - field_is_for_all: Za vse projekte - field_possible_values: Možne vrednosti - field_regexp: Regularni izraz - field_min_length: Minimalna dolžina - field_max_length: Maksimalna dolžina - field_value: Vrednost - field_category: Kategorija - field_title: Naslov - field_project: Projekt - field_issue: Zahtevek - field_status: Status - field_notes: Zabeležka - field_is_closed: Zahtevek zaprt - field_is_default: Privzeta vrednost - field_tracker: Vrsta zahtevka - field_subject: Tema - field_due_date: Do datuma - field_assigned_to: Dodeljen - field_priority: Prioriteta - field_fixed_version: Ciljna verzija - field_user: Uporabnik - field_role: Vloga - field_homepage: DomaÄa stran - field_is_public: Javno - field_parent: Podprojekt projekta - field_is_in_roadmap: Zahtevki prikazani na zemljevidu - field_login: Prijava - field_mail_notification: E-poÅ¡tna oznanila - field_admin: Administrator - field_last_login_on: ZadnjiÄ povezan(a) - field_language: Jezik - field_effective_date: Datum - field_password: Geslo - field_new_password: Novo geslo - field_password_confirmation: Potrditev - field_version: Verzija - field_type: Tip - field_host: Gostitelj - field_port: Vrata - field_account: RaÄun - field_base_dn: Bazni DN - field_attr_login: Oznaka za prijavo - field_attr_firstname: Oznaka za ime - field_attr_lastname: Oznaka za priimek - field_attr_mail: Oznaka za e-naslov - field_onthefly: Sprotna izdelava uporabnikov - field_start_date: ZaÄetek - field_done_ratio: "% Narejeno" - field_auth_source: NaÄin overovljanja - field_hide_mail: Skrij moj e-naslov - field_comments: Komentar - field_url: URL - field_start_page: ZaÄetna stran - field_subproject: Podprojekt - field_hours: Ur - field_activity: Aktivnost - field_spent_on: Datum - field_identifier: Identifikator - field_is_filter: Uporabljen kot filter - field_issue_to: Povezan zahtevek - field_delay: Zamik - field_assignable: Zahtevki so lahko dodeljeni tej vlogi - field_redirect_existing_links: Preusmeri obstojeÄe povezave - field_estimated_hours: Ocenjen Äas - field_column_names: Stolpci - field_time_zone: ÄŒasovni pas - field_searchable: Zmožen iskanja - field_default_value: Privzeta vrednost - field_comments_sorting: Prikaži komentarje - field_parent_title: MatiÄna stran - - setting_app_title: Naslov aplikacije - setting_app_subtitle: Podnaslov aplikacije - setting_welcome_text: Pozdravno besedilo - setting_default_language: Privzeti jezik - setting_login_required: Zahtevano overovljanje - setting_self_registration: Samostojna registracija - setting_attachment_max_size: Maksimalna velikost priponk - setting_issues_export_limit: Skrajna meja za izvoz zahtevkov - setting_mail_from: E-naslov za emisijo - setting_bcc_recipients: Prejemniki slepih kopij (bcc) - setting_plain_text_mail: navadno e-sporoÄilo (ne HTML) - setting_host_name: Ime gostitelja in pot - setting_text_formatting: Oblikovanje besedila - setting_wiki_compression: Stiskanje Wiki zgodovine - setting_feeds_limit: Meja obsega RSS virov - setting_default_projects_public: Novi projekti so privzeto javni - setting_autofetch_changesets: Samodejni izvleÄek zapisa sprememb - setting_sys_api_enabled: OmogoÄi WS za upravljanje shrambe - setting_commit_ref_keywords: Sklicne kljuÄne besede - setting_commit_fix_keywords: Urejanje kljuÄne besede - setting_autologin: Avtomatska prijava - setting_date_format: Oblika datuma - setting_time_format: Oblika Äasa - setting_cross_project_issue_relations: Dovoli povezave zahtevkov med razliÄnimi projekti - setting_issue_list_default_columns: Privzeti stolpci prikazani na seznamu zahtevkov - setting_emails_footer: Noga e-sporoÄil - setting_protocol: Protokol - setting_per_page_options: Å tevilo elementov na stran - setting_user_format: Oblika prikaza uporabnikov - setting_activity_days_default: Prikaz dni na aktivnost projekta - setting_display_subprojects_issues: Privzeti prikaz zahtevkov podprojektov v glavnem projektu - setting_enabled_scm: OmogoÄen SCM - setting_mail_handler_api_enabled: OmogoÄi WS za prihajajoÄo e-poÅ¡to - setting_mail_handler_api_key: API kljuÄ - setting_sequential_project_identifiers: Generiraj projektne identifikatorje sekvenÄno - setting_gravatar_enabled: Uporabljaj Gravatar ikone - setting_diff_max_lines_displayed: Maksimalno Å¡tevilo prikazanih vrstic razliÄnosti - - permission_edit_project: Uredi projekt - permission_select_project_modules: Izberi module projekta - permission_manage_members: Uredi Älane - permission_manage_versions: Uredi verzije - permission_manage_categories: Urejanje kategorij zahtevkov - permission_add_issues: Dodaj zahtevke - permission_edit_issues: Uredi zahtevke - permission_manage_issue_relations: Uredi odnose med zahtevki - permission_add_issue_notes: Dodaj zabeležke - permission_edit_issue_notes: Uredi zabeležke - permission_edit_own_issue_notes: Uredi lastne zabeležke - permission_move_issues: Premakni zahtevke - permission_delete_issues: IzbriÅ¡i zahtevke - permission_manage_public_queries: Uredi javna povpraÅ¡evanja - permission_save_queries: Shrani povpraÅ¡evanje - permission_view_gantt: Poglej gantogram - permission_view_calendar: Poglej koledar - permission_view_issue_watchers: Oglej si listo spremeljevalcev - permission_add_issue_watchers: Dodaj spremljevalce - permission_log_time: Beleži porabljen Äas - permission_view_time_entries: Poglej porabljen Äas - permission_edit_time_entries: Uredi beležko Äasa - permission_edit_own_time_entries: Uredi beležko lastnega Äasa - permission_manage_news: Uredi novice - permission_comment_news: Komentiraj novice - permission_manage_documents: Uredi dokumente - permission_view_documents: Poglej dokumente - permission_manage_files: Uredi datoteke - permission_view_files: Poglej datoteke - permission_manage_wiki: Uredi wiki - permission_rename_wiki_pages: Preimenuj wiki strani - permission_delete_wiki_pages: IzbriÅ¡i wiki strani - permission_view_wiki_pages: Poglej wiki - permission_view_wiki_edits: Poglej wiki zgodovino - permission_edit_wiki_pages: Uredi wiki strani - permission_delete_wiki_pages_attachments: IzbriÅ¡i priponke - permission_protect_wiki_pages: ZaÅ¡Äiti wiki strani - permission_manage_repository: Uredi shrambo - permission_browse_repository: Prebrskaj shrambo - permission_view_changesets: Poglej zapis sprememb - permission_commit_access: Dostop za predajo - permission_manage_boards: Uredi table - permission_view_messages: Poglej sporoÄila - permission_add_messages: Objavi sporoÄila - permission_edit_messages: Uredi sporoÄila - permission_edit_own_messages: Uredi lastna sporoÄila - permission_delete_messages: IzbriÅ¡i sporoÄila - permission_delete_own_messages: IzbriÅ¡i lastna sporoÄila - - project_module_issue_tracking: Sledenje zahtevkom - project_module_time_tracking: Sledenje Äasa - project_module_news: Novice - project_module_documents: Dokumenti - project_module_files: Datoteke - project_module_wiki: Wiki - project_module_repository: Shramba - project_module_boards: Table - - label_user: Uporabnik - label_user_plural: Uporabniki - label_user_new: Nov uporabnik - label_project: Projekt - label_project_new: Nov projekt - label_project_plural: Projekti - label_x_projects: - zero: ni projektov - one: 1 projekt - other: "%{count} projektov" - label_project_all: Vsi projekti - label_project_latest: Zadnji projekti - label_issue: Zahtevek - label_issue_new: Nov zahtevek - label_issue_plural: Zahtevki - label_issue_view_all: Poglej vse zahtevke - label_issues_by: "Zahtevki od %{value}" - label_issue_added: Zahtevek dodan - label_issue_updated: Zahtevek posodobljen - label_document: Dokument - label_document_new: Nov dokument - label_document_plural: Dokumenti - label_document_added: Dokument dodan - label_role: Vloga - label_role_plural: Vloge - label_role_new: Nova vloga - label_role_and_permissions: Vloge in dovoljenja - label_member: ÄŒlan - label_member_new: Nov Älan - label_member_plural: ÄŒlani - label_tracker: Vrsta zahtevka - label_tracker_plural: Vrste zahtevkov - label_tracker_new: Nova vrsta zahtevka - label_workflow: Potek dela - label_issue_status: Stanje zahtevka - label_issue_status_plural: Stanje zahtevkov - label_issue_status_new: Novo stanje - label_issue_category: Kategorija zahtevka - label_issue_category_plural: Kategorije zahtevkov - label_issue_category_new: Nova kategorija - label_custom_field: Polje po meri - label_custom_field_plural: Polja po meri - label_custom_field_new: Novo polje po meri - label_enumerations: Seznami - label_enumeration_new: Nova vrednost - label_information: Informacija - label_information_plural: Informacije - label_please_login: Prosimo prijavite se - label_register: Registracija - label_password_lost: Izgubljeno geslo - label_home: Domov - label_my_page: Moja stran - label_my_account: Moj raÄun - label_my_projects: Moji projekti - label_administration: Upravljanje - label_login: Prijavi se - label_logout: Odjavi se - label_help: PomoÄ - label_reported_issues: Prijavljeni zahtevki - label_assigned_to_me_issues: Zahtevki dodeljeni meni - label_last_login: Zadnja povezava - label_registered_on: Registriran - label_activity: Aktivnost - label_overall_activity: Celotna aktivnost - label_user_activity: "Aktivnost %{value}" - label_new: Nov - label_logged_as: Prijavljen(a) kot - label_environment: Okolje - label_authentication: Overovitev - label_auth_source: NaÄin overovitve - label_auth_source_new: Nov naÄin overovitve - label_auth_source_plural: NaÄini overovitve - label_subproject_plural: Podprojekti - label_and_its_subprojects: "%{value} in njegovi podprojekti" - label_min_max_length: Min - Max dolžina - label_list: Seznam - label_date: Datum - label_integer: Celo Å¡tevilo - label_float: Decimalno Å¡tevilo - label_boolean: Boolean - label_string: Besedilo - label_text: Dolgo besedilo - label_attribute: Lastnost - label_attribute_plural: Lastnosti - label_download: "%{count} Prenos" - label_download_plural: "%{count} Prenosi" - label_no_data: Ni podatkov za prikaz - label_change_status: Spremeni stanje - label_history: Zgodovina - label_attachment: Datoteka - label_attachment_new: Nova datoteka - label_attachment_delete: IzbriÅ¡i datoteko - label_attachment_plural: Datoteke - label_file_added: Datoteka dodana - label_report: PoroÄilo - label_report_plural: PoroÄila - label_news: Novica - label_news_new: Dodaj novico - label_news_plural: Novice - label_news_latest: Zadnje novice - label_news_view_all: Poglej vse novice - label_news_added: Dodane novice - label_settings: Nastavitve - label_overview: Pregled - label_version: Verzija - label_version_new: Nova verzija - label_version_plural: Verzije - label_confirmation: Potrditev - label_export_to: 'Na razpolago tudi v:' - label_read: Preberi... - label_public_projects: Javni projekti - label_open_issues: odprt zahtevek - label_open_issues_plural: odprti zahtevki - label_closed_issues: zaprt zahtevek - label_closed_issues_plural: zaprti zahtevki - label_x_open_issues_abbr_on_total: - zero: 0 odprtih / %{total} - one: 1 odprt / %{total} - other: "%{count} odprtih / %{total}" - label_x_open_issues_abbr: - zero: 0 odprtih - one: 1 odprt - other: "%{count} odprtih" - label_x_closed_issues_abbr: - zero: 0 zaprtih - one: 1 zaprt - other: "%{count} zaprtih" - label_total: Skupaj - label_permissions: Dovoljenja - label_current_status: Trenutno stanje - label_new_statuses_allowed: Novi zahtevki dovoljeni - label_all: vsi - label_none: noben - label_nobody: nihÄe - label_next: Naslednji - label_previous: PrejÅ¡nji - label_used_by: V uporabi od - label_details: Podrobnosti - label_add_note: Dodaj zabeležko - label_per_page: Na stran - label_calendar: Koledar - label_months_from: mesecev od - label_gantt: Gantogram - label_internal: Notranji - label_last_changes: "zadnjih %{count} sprememb" - label_change_view_all: Poglej vse spremembe - label_personalize_page: Individualiziraj to stran - label_comment: Komentar - label_comment_plural: Komentarji - label_x_comments: - zero: ni komentarjev - one: 1 komentar - other: "%{count} komentarjev" - label_comment_add: Dodaj komentar - label_comment_added: Komentar dodan - label_comment_delete: IzbriÅ¡i komentarje - label_query: Iskanje po meri - label_query_plural: Iskanja po meri - label_query_new: Novo iskanje - label_filter_add: Dodaj filter - label_filter_plural: Filtri - label_equals: je enako - label_not_equals: ni enako - label_in_less_than: v manj kot - label_in_more_than: v veÄ kot - label_in: v - label_today: danes - label_all_time: v vsem Äasu - label_yesterday: vÄeraj - label_this_week: ta teden - label_last_week: pretekli teden - label_last_n_days: "zadnjih %{count} dni" - label_this_month: ta mesec - label_last_month: zadnji mesec - label_this_year: to leto - label_date_range: Razpon datumov - label_less_than_ago: manj kot dni nazaj - label_more_than_ago: veÄ kot dni nazaj - label_ago: dni nazaj - label_contains: vsebuje - label_not_contains: ne vsebuje - label_day_plural: dni - label_repository: Shramba - label_repository_plural: Shrambe - label_browse: Prebrskaj - label_modification: "%{count} sprememba" - label_modification_plural: "%{count} spremembe" - label_revision: Revizija - label_revision_plural: Revizije - label_associated_revisions: Povezane revizije - label_added: dodano - label_modified: spremenjeno - label_copied: kopirano - label_renamed: preimenovano - label_deleted: izbrisano - label_latest_revision: Zadnja revizija - label_latest_revision_plural: Zadnje revizije - label_view_revisions: Poglej revizije - label_max_size: NajveÄja velikost - label_sort_highest: Premakni na vrh - label_sort_higher: Premakni gor - label_sort_lower: Premakni dol - label_sort_lowest: Premakni na dno - label_roadmap: NaÄrt - label_roadmap_due_in: "Do %{value}" - label_roadmap_overdue: "%{value} zakasnel" - label_roadmap_no_issues: Ni zahtevkov za to verzijo - label_search: IÅ¡Äi - label_result_plural: Rezultati - label_all_words: Vse besede - label_wiki: Wiki - label_wiki_edit: Wiki urejanje - label_wiki_edit_plural: Wiki urejanja - label_wiki_page: Wiki stran - label_wiki_page_plural: Wiki strani - label_index_by_title: Razvrsti po naslovu - label_index_by_date: Razvrsti po datumu - label_current_version: Trenutna verzija - label_preview: Predogled - label_feed_plural: RSS viri - label_changes_details: Podrobnosti o vseh spremembah - label_issue_tracking: Sledenje zahtevkom - label_spent_time: Porabljen Äas - label_f_hour: "%{value} ura" - label_f_hour_plural: "%{value} ur" - label_time_tracking: Sledenje Äasu - label_change_plural: Spremembe - label_statistics: Statistika - label_commits_per_month: Predaj na mesec - label_commits_per_author: Predaj na avtorja - label_view_diff: Preglej razlike - label_diff_inline: znotraj - label_diff_side_by_side: vzporedno - label_options: Možnosti - label_copy_workflow_from: Kopiraj potek dela od - label_permissions_report: PoroÄilo o dovoljenjih - label_watched_issues: Spremljani zahtevki - label_related_issues: Povezani zahtevki - label_applied_status: Uveljavljeno stanje - label_loading: Nalaganje... - label_relation_new: Nova povezava - label_relation_delete: IzbriÅ¡i povezavo - label_relates_to: povezan z - label_duplicates: duplikati - label_duplicated_by: dupliciral - label_blocks: blok - label_blocked_by: blokiral - label_precedes: ima prednost pred - label_follows: sledi - label_end_to_start: konec na zaÄetek - label_end_to_end: konec na konec - label_start_to_start: zaÄetek na zaÄetek - label_start_to_end: zaÄetek na konec - label_stay_logged_in: Ostani prijavljen(a) - label_disabled: onemogoÄi - label_show_completed_versions: Prikaži zakljuÄene verzije - label_me: jaz - label_board: Forum - label_board_new: Nov forum - label_board_plural: Forumi - label_topic_plural: Teme - label_message_plural: SporoÄila - label_message_last: Zadnje sporoÄilo - label_message_new: Novo sporoÄilo - label_message_posted: SporoÄilo dodano - label_reply_plural: Odgovori - label_send_information: PoÅ¡lji informacijo o raÄunu uporabniku - label_year: Leto - label_month: Mesec - label_week: Teden - label_date_from: Do - label_date_to: Do - label_language_based: Glede na uporabnikov jezik - label_sort_by: "Razporedi po %{value}" - label_send_test_email: PoÅ¡lji testno e-pismo - label_feeds_access_key_created_on: "RSS dostopni kljuÄ narejen %{value} nazaj" - label_module_plural: Moduli - label_added_time_by: "Dodan %{author} %{age} nazaj" - label_updated_time_by: "Posodobljen od %{author} %{age} nazaj" - label_updated_time: "Posodobljen %{value} nazaj" - label_jump_to_a_project: SkoÄi na projekt... - label_file_plural: Datoteke - label_changeset_plural: Zapisi sprememb - label_default_columns: Privzeti stolpci - label_no_change_option: (Ni spremembe) - label_bulk_edit_selected_issues: Uredi izbrane zahtevke skupaj - label_theme: Tema - label_default: Privzeto - label_search_titles_only: PreiÅ¡Äi samo naslove - label_user_mail_option_all: "Za vsak dogodek v vseh mojih projektih" - label_user_mail_option_selected: "Za vsak dogodek samo na izbranih projektih..." - label_user_mail_no_self_notified: "Ne želim biti opozorjen(a) na spremembe, ki jih naredim sam(a)" - label_registration_activation_by_email: aktivacija raÄuna po e-poÅ¡ti - label_registration_manual_activation: roÄna aktivacija raÄuna - label_registration_automatic_activation: samodejna aktivacija raÄuna - label_display_per_page: "Na stran: %{value}" - label_age: Starost - label_change_properties: Sprememba lastnosti - label_general: SploÅ¡no - label_more: VeÄ - label_scm: SCM - label_plugins: VtiÄniki - label_ldap_authentication: LDAP overovljanje - label_downloads_abbr: D/L - label_optional_description: Neobvezen opis - label_add_another_file: Dodaj Å¡e eno datoteko - label_preferences: Preference - label_chronological_order: KronoloÅ¡ko - label_reverse_chronological_order: Obrnjeno kronoloÅ¡ko - label_planning: NaÄrtovanje - label_incoming_emails: PrihajajoÄa e-poÅ¡ta - label_generate_key: Ustvari kljuÄ - label_issue_watchers: Spremljevalci - label_example: Vzorec - - button_login: Prijavi se - button_submit: PoÅ¡lji - button_save: Shrani - button_check_all: OznaÄi vse - button_uncheck_all: OdznaÄi vse - button_delete: IzbriÅ¡i - button_create: Ustvari - button_test: Testiraj - button_edit: Uredi - button_add: Dodaj - button_change: Spremeni - button_apply: Uporabi - button_clear: PoÄisti - button_lock: Zakleni - button_unlock: Odkleni - button_download: Prenesi - button_list: Seznam - button_view: Pogled - button_move: Premakni - button_back: Nazaj - button_cancel: PrekliÄi - button_activate: Aktiviraj - button_sort: Razvrsti - button_log_time: Beleži Äas - button_rollback: Povrni na to verzijo - button_watch: Spremljaj - button_unwatch: Ne spremljaj - button_reply: Odgovori - button_archive: Arhiviraj - button_unarchive: Odarhiviraj - button_reset: Ponastavi - button_rename: Preimenuj - button_change_password: Spremeni geslo - button_copy: Kopiraj - button_annotate: ZapiÅ¡i pripombo - button_update: Posodobi - button_configure: Konfiguriraj - button_quote: Citiraj - - status_active: aktivni - status_registered: registriran - status_locked: zaklenjen - - text_select_mail_notifications: Izberi dejanja za katera naj bodo poslana oznanila preko e-poÅ¡to. - text_regexp_info: npr. ^[A-Z0-9]+$ - text_min_max_length_info: 0 pomeni brez omejitev - text_project_destroy_confirmation: Ali ste prepriÄani da želite izbrisati izbrani projekt in vse z njim povezane podatke? - text_subprojects_destroy_warning: "Njegov(i) podprojekt(i): %{value} bodo prav tako izbrisani." - text_workflow_edit: Izberite vlogo in zahtevek za urejanje poteka dela - text_are_you_sure: Ali ste prepriÄani? - text_tip_issue_begin_day: naloga z zaÄetkom na ta dan - text_tip_issue_end_day: naloga z zakljuÄkom na ta dan - text_tip_issue_begin_end_day: naloga ki se zaÄne in konÄa ta dan - text_caracters_maximum: "najveÄ %{count} znakov." - text_caracters_minimum: "Mora biti vsaj dolg vsaj %{count} znakov." - text_length_between: "Dolžina med %{min} in %{max} znakov." - text_tracker_no_workflow: Potek dela za to vrsto zahtevka ni doloÄen - text_unallowed_characters: Nedovoljeni znaki - text_comma_separated: Dovoljenih je veÄ vrednosti (loÄenih z vejico). - text_issues_ref_in_commit_messages: Zahtevki sklicev in popravkov v sporoÄilu predaje - text_issue_added: "Zahtevek %{id} je sporoÄil(a) %{author}." - text_issue_updated: "Zahtevek %{id} je posodobil(a) %{author}." - text_wiki_destroy_confirmation: Ali ste prepriÄani da želite izbrisati ta wiki in vso njegovo vsebino? - text_issue_category_destroy_question: "Nekateri zahtevki (%{count}) so dodeljeni tej kategoriji. Kaj želite storiti?" - text_issue_category_destroy_assignments: Odstrani naloge v kategoriji - text_issue_category_reassign_to: Ponovno dodeli zahtevke tej kategoriji - text_user_mail_option: "Na neizbrane projekte boste prejemali le obvestila o zadevah ki jih spremljate ali v katere ste vkljuÄeni (npr. zahtevki katerih avtor(ica) ste)" - text_no_configuration_data: "Vloge, vrste zahtevkov, statusi zahtevkov in potek dela Å¡e niso bili doloÄeni. \nZelo priporoÄljivo je, da naložite privzeto konfiguracijo, ki jo lahko kasneje tudi prilagodite." - text_load_default_configuration: Naloži privzeto konfiguracijo - text_status_changed_by_changeset: "Dodano v zapis sprememb %{value}." - text_issues_destroy_confirmation: 'Ali ste prepriÄani, da želite izbrisati izbrani(e) zahtevek(ke)?' - text_select_project_modules: 'Izberite module, ki jih želite omogoÄiti za ta projekt:' - text_default_administrator_account_changed: Spremenjen privzeti administratorski raÄun - text_file_repository_writable: OmogoÄeno pisanje v shrambo datotek - text_rmagick_available: RMagick je na voljo(neobvezno) - text_destroy_time_entries_question: "%{hours} ur je bilo opravljenih na zahtevku, ki ga želite izbrisati. Kaj želite storiti?" - text_destroy_time_entries: IzbriÅ¡i opravljene ure - text_assign_time_entries_to_project: Predaj opravljene ure projektu - text_reassign_time_entries: 'Prenesi opravljene ure na ta zahtevek:' - text_user_wrote: "%{value} je napisal(a):" - text_enumeration_destroy_question: "%{count} objektov je doloÄenih tej vrednosti." - text_enumeration_category_reassign_to: 'Ponastavi jih na to vrednost:' - text_email_delivery_not_configured: "E-poÅ¡tna dostava ni nastavljena in oznanila so onemogoÄena.\nNastavite vaÅ¡ SMTP strežnik v config/configuration.yml in ponovno zaženite aplikacijo da ga omogoÄite.\n" - text_repository_usernames_mapping: "Izberite ali posodobite Redmine uporabnika dodeljenega vsakemu uporabniÅ¡kemu imenu najdenemu v zapisniku shrambe.\n Uporabniki z enakim Redmine ali shrambinem uporabniÅ¡kem imenu ali e-poÅ¡tnem naslovu so samodejno dodeljeni." - text_diff_truncated: '... Ta sprememba je bila odsekana ker presega najveÄjo velikost ki je lahko prikazana.' - - default_role_manager: Upravnik - default_role_developer: Razvijalec - default_role_reporter: PoroÄevalec - default_tracker_bug: HroÅ¡Ä - default_tracker_feature: Funkcija - default_tracker_support: Podpora - default_issue_status_new: Nov - default_issue_status_in_progress: V teku - default_issue_status_resolved: ReÅ¡en - default_issue_status_feedback: Povratna informacija - default_issue_status_closed: ZakljuÄen - default_issue_status_rejected: Zavrnjen - default_doc_category_user: UporabniÅ¡ka dokumentacija - default_doc_category_tech: TehniÄna dokumentacija - default_priority_low: Nizka - default_priority_normal: ObiÄajna - default_priority_high: Visoka - default_priority_urgent: Urgentna - default_priority_immediate: TakojÅ¡nje ukrepanje - default_activity_design: Oblikovanje - default_activity_development: Razvoj - - enumeration_issue_priorities: Prioritete zahtevkov - enumeration_doc_categories: Kategorije dokumentov - enumeration_activities: Aktivnosti (sledenje Äasa) - warning_attachments_not_saved: "%{count} datotek(e) ni bilo mogoÄe shraniti." - field_editable: Uredljivo - text_plugin_assets_writable: Zapisljiva mapa za vtiÄnike - label_display: Prikaz - button_create_and_continue: Ustvari in nadaljuj - text_custom_field_possible_values_info: 'Ena vrstica za vsako vrednost' - setting_repository_log_display_limit: NajveÄje Å¡tevilo prikazanih revizij v log datoteki - setting_file_max_size_displayed: NajveÄja velikost besedilnih datotek v vkljuÄenem prikazu - field_watcher: Opazovalec - setting_openid: Dovoli OpenID prijavo in registracijo - field_identity_url: OpenID URL - label_login_with_open_id_option: ali se prijavi z OpenID - field_content: Vsebina - label_descending: PadajoÄe - label_sort: Razvrsti - label_ascending: NaraÅ¡ÄajoÄe - label_date_from_to: Od %{start} do %{end} - label_greater_or_equal: ">=" - label_less_or_equal: <= - text_wiki_page_destroy_question: Ta stran ima %{descendants} podstran(i) in naslednik(ov). Kaj želite storiti? - text_wiki_page_reassign_children: Znova dodeli podstrani tej glavni strani - text_wiki_page_nullify_children: Obdrži podstrani kot glavne strani - text_wiki_page_destroy_children: IzbriÅ¡i podstrani in vse njihove naslednike - setting_password_min_length: Minimalna dolžina gesla - field_group_by: Združi rezultate po - mail_subject_wiki_content_updated: "'%{id}' wiki stran je bila posodobljena" - label_wiki_content_added: Wiki stran dodana - mail_subject_wiki_content_added: "'%{id}' wiki stran je bila dodana" - mail_body_wiki_content_added: "%{author} je dodal '%{id}' wiki stran" - label_wiki_content_updated: Wiki stran posodobljena - mail_body_wiki_content_updated: "%{author} je posodobil '%{id}' wiki stran." - permission_add_project: Ustvari projekt - setting_new_project_user_role_id: Vloga, dodeljena neadministratorskemu uporabniku, ki je ustvaril projekt - label_view_all_revisions: Poglej vse revizije - label_tag: Oznaka - label_branch: Veja - error_no_tracker_in_project: Noben sledilnik ni povezan s tem projektom. Prosimo preverite nastavitve projekta. - error_no_default_issue_status: Privzeti zahtevek ni definiran. Prosimo preverite svoje nastavitve (Pojdite na "Administracija -> Stanje zahtevkov"). - text_journal_changed: "%{label} se je spremenilo iz %{old} v %{new}" - text_journal_set_to: "%{label} nastavljeno na %{value}" - text_journal_deleted: "%{label} izbrisan (%{old})" - label_group_plural: Skupine - label_group: Skupina - label_group_new: Nova skupina - label_time_entry_plural: Porabljen Äas - text_journal_added: "%{label} %{value} dodan" - field_active: Aktiven - enumeration_system_activity: Sistemska aktivnost - permission_delete_issue_watchers: IzbriÅ¡i opazovalce - version_status_closed: zaprt - version_status_locked: zaklenjen - version_status_open: odprt - error_can_not_reopen_issue_on_closed_version: Zahtevek dodeljen zaprti verziji ne more biti ponovno odprt - label_user_anonymous: Anonimni - button_move_and_follow: Premakni in sledi - setting_default_projects_modules: Privzeti moduli za nove projekte - setting_gravatar_default: Privzeta Gravatar slika - field_sharing: Deljenje - label_version_sharing_hierarchy: S projektno hierarhijo - label_version_sharing_system: Z vsemi projekti - label_version_sharing_descendants: S podprojekti - label_version_sharing_tree: Z drevesom projekta - label_version_sharing_none: Ni deljeno - error_can_not_archive_project: Ta projekt ne more biti arhiviran - button_duplicate: Podvoji - button_copy_and_follow: Kopiraj in sledi - label_copy_source: Vir - setting_issue_done_ratio: IzraÄunaj razmerje opravljenega zahtevka z - setting_issue_done_ratio_issue_status: Uporabi stanje zahtevka - error_issue_done_ratios_not_updated: Razmerje opravljenega zahtevka ni bilo posodobljeno. - error_workflow_copy_target: Prosimo izberite ciljni(e) sledilnik(e) in vlogo(e) - setting_issue_done_ratio_issue_field: Uporabi polje zahtevka - label_copy_same_as_target: Enako kot cilj - label_copy_target: Cilj - notice_issue_done_ratios_updated: Razmerje opravljenega zahtevka posodobljeno. - error_workflow_copy_source: Prosimo izberite vir zahtevka ali vlogo - label_update_issue_done_ratios: Posodobi razmerje opravljenega zahtevka - setting_start_of_week: ZaÄni koledarje z - permission_view_issues: Poglej zahtevke - label_display_used_statuses_only: Prikaži samo stanja ki uporabljajo ta sledilnik - label_revision_id: Revizija %{value} - label_api_access_key: API dostopni kljuÄ - label_api_access_key_created_on: API dostopni kljuÄ ustvarjen pred %{value} - label_feeds_access_key: RSS dostopni kljuÄ - notice_api_access_key_reseted: VaÅ¡ API dostopni kljuÄ je bil ponastavljen. - setting_rest_api_enabled: OmogoÄi REST spletni servis - label_missing_api_access_key: ManjkajoÄ API dostopni kljuÄ - label_missing_feeds_access_key: ManjkajoÄ RSS dostopni kljuÄ - button_show: Prikaži - text_line_separated: Dovoljenih veÄ vrednosti (ena vrstica za vsako vrednost). - setting_mail_handler_body_delimiters: Odreži e-poÅ¡to po eni od teh vrstic - permission_add_subprojects: Ustvari podprojekte - label_subproject_new: Nov podprojekt - text_own_membership_delete_confirmation: |- - Odstranili boste nekatere ali vse od dovoljenj zaradi Äesar morda ne boste mogli veÄ urejati tega projekta. - Ali ste prepriÄani, da želite nadaljevati? - label_close_versions: Zapri dokonÄane verzije - label_board_sticky: Lepljivo - label_board_locked: Zaklenjeno - permission_export_wiki_pages: Izvozi wiki strani - setting_cache_formatted_text: Predpomni oblikovano besedilo - permission_manage_project_activities: Uredi aktivnosti projekta - error_unable_delete_issue_status: Stanja zahtevka ni bilo možno spremeniti - label_profile: Profil - permission_manage_subtasks: Uredi podnaloge - field_parent_issue: Nadrejena naloga - label_subtask_plural: Podnaloge - label_project_copy_notifications: Med kopiranjem projekta poÅ¡lji e-poÅ¡tno sporoÄilo - error_can_not_delete_custom_field: Polja po meri ni mogoÄe izbrisati - error_unable_to_connect: Povezava ni mogoÄa (%{value}) - error_can_not_remove_role: Ta vloga je v uporabi in je ni mogoÄe izbrisati. - error_can_not_delete_tracker: Ta sledilnik vsebuje zahtevke in se ga ne more izbrisati. - field_principal: Upravnik varnosti - label_my_page_block: Moj gradnik strani - notice_failed_to_save_members: "Shranjevanje uporabnika(ov) ni uspelo: %{errors}." - text_zoom_out: Približaj - text_zoom_in: Oddalji - notice_unable_delete_time_entry: Brisanje dnevnika porabljenaga Äasa ni mogoÄe. - label_overall_spent_time: Skupni porabljeni Äas - field_time_entries: Beleži porabljeni Äas - project_module_gantt: Gantogram - project_module_calendar: Koledear - button_edit_associated_wikipage: "Uredi povezano Wiki stran: %{page_title}" - field_text: Besedilno polje - label_user_mail_option_only_owner: Samo za stvari katerih lastnik sem - setting_default_notification_option: Privzeta možnost obveÅ¡Äanja - label_user_mail_option_only_my_events: Samo za stvari, ki jih opazujem ali sem v njih vpleten - label_user_mail_option_only_assigned: Samo za stvari, ki smo mi dodeljene - label_user_mail_option_none: Noben dogodek - field_member_of_group: PooblaÅ¡ÄenÄeva skupina - field_assigned_to_role: PooblaÅ¡ÄenÄeva vloga - notice_not_authorized_archived_project: Projekt, do katerega poskuÅ¡ate dostopati, je bil arhiviran. - label_principal_search: "PoiÅ¡Äi uporabnika ali skupino:" - label_user_search: "PoiÅ¡Äi uporabnikia:" - field_visible: Viden - setting_emails_header: Glava e-poÅ¡te - setting_commit_logtime_activity_id: Aktivnost zabeleženega Äasa - text_time_logged_by_changeset: Uporabljeno v spremembi %{value}. - setting_commit_logtime_enabled: OmogoÄi beleženje Äasa - notice_gantt_chart_truncated: Graf je bil odrezan, ker je prekoraÄil najveÄje dovoljeno Å¡tevilo elementov, ki se jih lahko prikaže (%{max}) - setting_gantt_items_limit: NajveÄje Å¡tevilo elementov prikazano na gantogramu - field_warn_on_leaving_unsaved: Opozori me, kadar zapuÅ¡Äam stran z neshranjenim besedilom - text_warn_on_leaving_unsaved: Trenutna stran vsebuje neshranjeno besedilo ki bo izgubljeno, Äe zapustite to stran. - label_my_queries: Moje poizvedbe po meri - text_journal_changed_no_detail: "%{label} posodobljen" - label_news_comment_added: Komentar dodan novici - button_expand_all: RazÅ¡iri vse - button_collapse_all: SkrÄi vse - label_additional_workflow_transitions_for_assignee: Dovoljeni dodatni prehodi kadar je uporabnik pooblaÅ¡Äenec - label_additional_workflow_transitions_for_author: Dovoljeni dodatni prehodi kadar je uporabnik avtor - label_bulk_edit_selected_time_entries: Skupinsko urejanje izbranih Äasovnih zapisov - text_time_entries_destroy_confirmation: Ali ste prepriÄani, da želite izbristai izbran(e) Äasovn(i/e) zapis(e)? - label_role_anonymous: Anonimni - label_role_non_member: NeÄlan - label_issue_note_added: Dodan zaznamek - label_issue_status_updated: Status posodobljen - label_issue_priority_updated: Prioriteta posodobljena - label_issues_visibility_own: Zahtevek ustvarjen s strani uporabnika ali dodeljen uporabniku - field_issues_visibility: Vidljivost zahtevkov - label_issues_visibility_all: Vsi zahtevki - permission_set_own_issues_private: Nastavi lastne zahtevke kot javne ali zasebne - field_is_private: Zaseben - permission_set_issues_private: Nastavi zahtevke kot javne ali zasebne - label_issues_visibility_public: Vsi nezasebni zahtevki - text_issues_destroy_descendants_confirmation: To bo izbrisalo tudi %{count} podnalog(o). - field_commit_logs_encoding: Kodiranje sporoÄil ob predaji - field_scm_path_encoding: Pot do kodiranja - text_scm_path_encoding_note: "Privzeto: UTF-8" - field_path_to_repository: Pot do shrambe - field_root_directory: Korenska mapa - field_cvs_module: Modul - field_cvsroot: CVSROOT - text_mercurial_repository_note: Lokalna shramba (npr. /hgrepo, c:\hgrepo) - text_scm_command: Ukaz - text_scm_command_version: Verzija - label_git_report_last_commit: SporoÄi zadnje uveljavljanje datotek in map - text_scm_config: Svoje SCM ukaze lahko nastavite v datoteki config/configuration.yml. Po urejanju prosimo ponovno zaženite aplikacijo. - text_scm_command_not_available: SCM ukaz ni na voljo. Prosimo preverite nastavitve v upravljalskem podoknu. - - text_git_repository_note: Shramba je prazna in lokalna (npr. /gitrepo, c:\gitrepo) - - notice_issue_successful_create: Ustvarjen zahtevek %{id}. - label_between: med - setting_issue_group_assignment: Dovoli dodeljevanje zahtevka skupinam - label_diff: diff - - 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 zahtevek - one: 1 zahtevek - other: "%{count} zahtevki" - 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.
    Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account - text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: vsi - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: S podprojekti - label_cross_project_tree: Z drevesom projekta - label_cross_project_hierarchy: S projektno hierarhijo - label_cross_project_system: Z vsemi projekti - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d5c5458ec41dd3315a619640122843b9e0e9952.svn-base --- a/.svn/pristine/2d/2d5c5458ec41dd3315a619640122843b9e0e9952.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'action_view/helpers/form_helper' - -class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder - include Redmine::I18n - - (field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for) + - %w(date_select)).each do |selector| - src = <<-END_SRC - def #{selector}(field, options = {}) - label_for_field(field, options) + super(field, options.except(:label)).html_safe - end - END_SRC - class_eval src, __FILE__, __LINE__ - end - - def select(field, choices, options = {}, html_options = {}) - label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe - end - - def time_zone_select(field, priority_zones = nil, options = {}, html_options = {}) - label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe - end - - # Returns a label tag for the given field - def label_for_field(field, options = {}) - return ''.html_safe if options.delete(:no_label) - text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label] - text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) - text += @template.content_tag("span", " *", :class => "required") if options.delete(:required) - @template.content_tag("label", text.html_safe, - :class => (@object && @object.errors[field].present? ? "error" : nil), - :for => (@object_name.to_s + "_" + field.to_s)) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d6ad8e6f87f6d379c227b9a81e69b5a26399856.svn-base --- a/.svn/pristine/2d/2d6ad8e6f87f6d379c227b9a81e69b5a26399856.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,254 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'account_controller' - -# Re-raise errors caught by the controller. -class AccountController; def rescue_action(e) raise e end; end - -class AccountControllerTest < ActionController::TestCase - fixtures :users, :roles - - def setup - @controller = AccountController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - end - - def test_get_login - get :login - assert_response :success - assert_template 'login' - - assert_select 'input[name=username]' - assert_select 'input[name=password]' - end - - def test_login_should_redirect_to_back_url_param - # request.uri is "test.host" in test environment - post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1' - assert_redirected_to '/issues/show/1' - end - - def test_login_should_not_redirect_to_another_host - post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake' - assert_redirected_to '/my/page' - end - - def test_login_with_wrong_password - post :login, :username => 'admin', :password => 'bad' - assert_response :success - assert_template 'login' - - assert_select 'div.flash.error', :text => /Invalid user or password/ - assert_select 'input[name=username][value=admin]' - assert_select 'input[name=password]' - assert_select 'input[name=password][value]', 0 - end - - def test_login_should_rescue_auth_source_exception - source = AuthSource.create!(:name => 'Test') - User.find(2).update_attribute :auth_source_id, source.id - AuthSource.any_instance.stubs(:authenticate).raises(AuthSourceException.new("Something wrong")) - - post :login, :username => 'jsmith', :password => 'jsmith' - assert_response 500 - assert_error_tag :content => /Something wrong/ - end - - def test_login_should_reset_session - @controller.expects(:reset_session).once - - post :login, :username => 'jsmith', :password => 'jsmith' - assert_response 302 - end - - def test_logout - @request.session[:user_id] = 2 - get :logout - assert_redirected_to '/' - assert_nil @request.session[:user_id] - end - - def test_logout_should_reset_session - @controller.expects(:reset_session).once - - @request.session[:user_id] = 2 - get :logout - assert_response 302 - end - - def test_get_register_with_registration_on - with_settings :self_registration => '3' do - get :register - assert_response :success - assert_template 'register' - assert_not_nil assigns(:user) - - assert_tag 'input', :attributes => {:name => 'user[password]'} - assert_tag 'input', :attributes => {:name => 'user[password_confirmation]'} - end - end - - def test_get_register_with_registration_off_should_redirect - with_settings :self_registration => '0' do - get :register - assert_redirected_to '/' - end - end - - # See integration/account_test.rb for the full test - def test_post_register_with_registration_on - with_settings :self_registration => '3' do - assert_difference 'User.count' do - post :register, :user => { - :login => 'register', - :password => 'secret123', - :password_confirmation => 'secret123', - :firstname => 'John', - :lastname => 'Doe', - :mail => 'register@example.com' - } - assert_redirected_to '/my/account' - end - user = User.first(:order => 'id DESC') - assert_equal 'register', user.login - assert_equal 'John', user.firstname - assert_equal 'Doe', user.lastname - assert_equal 'register@example.com', user.mail - assert user.check_password?('secret123') - assert user.active? - end - end - - def test_post_register_with_registration_off_should_redirect - with_settings :self_registration => '0' do - assert_no_difference 'User.count' do - post :register, :user => { - :login => 'register', - :password => 'test', - :password_confirmation => 'test', - :firstname => 'John', - :lastname => 'Doe', - :mail => 'register@example.com' - } - assert_redirected_to '/' - end - end - end - - def test_get_lost_password_should_display_lost_password_form - get :lost_password - assert_response :success - assert_select 'input[name=mail]' - end - - def test_lost_password_for_active_user_should_create_a_token - Token.delete_all - ActionMailer::Base.deliveries.clear - assert_difference 'ActionMailer::Base.deliveries.size' do - assert_difference 'Token.count' do - with_settings :host_name => 'mydomain.foo', :protocol => 'http' do - post :lost_password, :mail => 'JSmith@somenet.foo' - assert_redirected_to '/login' - end - end - end - - token = Token.order('id DESC').first - assert_equal User.find(2), token.user - assert_equal 'recovery', token.action - - assert_select_email do - assert_select "a[href=?]", "http://mydomain.foo/account/lost_password?token=#{token.value}" - end - end - - def test_lost_password_for_unknown_user_should_fail - Token.delete_all - assert_no_difference 'Token.count' do - post :lost_password, :mail => 'invalid@somenet.foo' - assert_response :success - end - end - - def test_lost_password_for_non_active_user_should_fail - Token.delete_all - assert User.find(2).lock! - - assert_no_difference 'Token.count' do - post :lost_password, :mail => 'JSmith@somenet.foo' - assert_response :success - end - end - - def test_get_lost_password_with_token_should_display_the_password_recovery_form - user = User.find(2) - token = Token.create!(:action => 'recovery', :user => user) - - get :lost_password, :token => token.value - assert_response :success - assert_template 'password_recovery' - - assert_select 'input[type=hidden][name=token][value=?]', token.value - end - - def test_get_lost_password_with_invalid_token_should_redirect - get :lost_password, :token => "abcdef" - assert_redirected_to '/' - end - - def test_post_lost_password_with_token_should_change_the_user_password - user = User.find(2) - token = Token.create!(:action => 'recovery', :user => user) - - post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' - assert_redirected_to '/login' - user.reload - assert user.check_password?('newpass123') - assert_nil Token.find_by_id(token.id), "Token was not deleted" - end - - def test_post_lost_password_with_token_for_non_active_user_should_fail - user = User.find(2) - token = Token.create!(:action => 'recovery', :user => user) - user.lock! - - post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' - assert_redirected_to '/' - assert ! user.check_password?('newpass123') - end - - def test_post_lost_password_with_token_and_password_confirmation_failure_should_redisplay_the_form - user = User.find(2) - token = Token.create!(:action => 'recovery', :user => user) - - post :lost_password, :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'wrongpass' - assert_response :success - assert_template 'password_recovery' - assert_not_nil Token.find_by_id(token.id), "Token was deleted" - - assert_select 'input[type=hidden][name=token][value=?]', token.value - end - - def test_post_lost_password_with_invalid_token_should_redirect - post :lost_password, :token => "abcdef", :new_password => 'newpass', :new_password_confirmation => 'newpass' - assert_redirected_to '/' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d704054566a75db6513ae575339e7bf14942054.svn-base --- a/.svn/pristine/2d/2d704054566a75db6513ae575339e7bf14942054.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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::WikiFormattingTest < ActiveSupport::TestCase - fixtures :issues - - def test_textile_formatter - assert_equal Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting.formatter_for('textile') - assert_equal Redmine::WikiFormatting::Textile::Helper, Redmine::WikiFormatting.helper_for('textile') - end - - def test_null_formatter - assert_equal Redmine::WikiFormatting::NullFormatter::Formatter, Redmine::WikiFormatting.formatter_for('') - assert_equal Redmine::WikiFormatting::NullFormatter::Helper, Redmine::WikiFormatting.helper_for('') - end - - def test_should_link_urls_and_email_addresses - raw = <<-DIFF -This is a sample *text* with a link: http://www.redmine.org -and an email address foo@example.net -DIFF - - expected = <<-EXPECTED -

    This is a sample *text* with a link: http://www.redmine.org
    -and an email address

    -EXPECTED - - assert_equal expected.gsub(%r{[\r\n\t]}, ''), Redmine::WikiFormatting::NullFormatter::Formatter.new(raw).to_html.gsub(%r{[\r\n\t]}, '') - end - - def test_supports_section_edit - with_settings :text_formatting => 'textile' do - assert_equal true, Redmine::WikiFormatting.supports_section_edit? - end - - with_settings :text_formatting => '' do - assert_equal false, Redmine::WikiFormatting.supports_section_edit? - end - end - - def test_cache_key_for_saved_object_should_no_be_nil - assert_not_nil Redmine::WikiFormatting.cache_key_for('textile', 'Text', Issue.find(1), :description) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d79f51bb3aabe1350946a7c8d7303968d415bb8.svn-base --- a/.svn/pristine/2d/2d79f51bb3aabe1350946a7c8d7303968d415bb8.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,229 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 IssuesTest < ActionController::IntegrationTest - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :trackers, - :projects_trackers, - :enabled_modules, - :issue_statuses, - :issues, - :enumerations, - :custom_fields, - :custom_values, - :custom_fields_trackers - - # create an issue - def test_add_issue - log_user('jsmith', 'jsmith') - get 'projects/1/issues/new', :tracker_id => '1' - assert_response :success - assert_template 'issues/new' - - post 'projects/1/issues', :tracker_id => "1", - :issue => { :start_date => "2006-12-26", - :priority_id => "4", - :subject => "new test issue", - :category_id => "", - :description => "new issue", - :done_ratio => "0", - :due_date => "", - :assigned_to_id => "" }, - :custom_fields => {'2' => 'Value for field 2'} - # find created issue - issue = Issue.find_by_subject("new test issue") - assert_kind_of Issue, issue - - # check redirection - assert_redirected_to :controller => 'issues', :action => 'show', :id => issue - follow_redirect! - assert_equal issue, assigns(:issue) - - # check issue attributes - assert_equal 'jsmith', issue.author.login - assert_equal 1, issue.project.id - assert_equal 1, issue.status.id - end - - def test_update_issue_form - log_user('jsmith', 'jsmith') - post 'projects/ecookbook/issues/new', :issue => { :tracker_id => "2"} - assert_response :success - assert_tag 'select', - :attributes => {:name => 'issue[tracker_id]'}, - :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}} - end - - # add then remove 2 attachments to an issue - def test_issue_attachments - log_user('jsmith', 'jsmith') - set_tmp_attachments_directory - - put 'issues/1', - :notes => 'Some notes', - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}} - assert_redirected_to "/issues/1" - - # make sure attachment was saved - attachment = Issue.find(1).attachments.find_by_filename("testfile.txt") - assert_kind_of Attachment, attachment - assert_equal Issue.find(1), attachment.container - assert_equal 'This is an attachment', attachment.description - # verify the size of the attachment stored in db - #assert_equal file_data_1.length, attachment.filesize - # verify that the attachment was written to disk - assert File.exist?(attachment.diskfile) - - # remove the attachments - Issue.find(1).attachments.each(&:destroy) - assert_equal 0, Issue.find(1).attachments.length - end - - def test_other_formats_links_on_index - get '/projects/ecookbook/issues' - - %w(Atom PDF CSV).each do |format| - assert_tag :a, :content => format, - :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}", - :rel => 'nofollow' } - end - end - - def test_other_formats_links_on_index_without_project_id_in_url - get '/issues', :project_id => 'ecookbook' - - %w(Atom PDF CSV).each do |format| - assert_tag :a, :content => format, - :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}", - :rel => 'nofollow' } - end - end - - def test_pagination_links_on_index - Setting.per_page_options = '2' - get '/projects/ecookbook/issues' - - assert_tag :a, :content => '2', - :attributes => { :href => '/projects/ecookbook/issues?page=2' } - - end - - def test_pagination_links_on_index_without_project_id_in_url - Setting.per_page_options = '2' - get '/issues', :project_id => 'ecookbook' - - assert_tag :a, :content => '2', - :attributes => { :href => '/projects/ecookbook/issues?page=2' } - - end - - def test_issue_with_user_custom_field - @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all) - Role.anonymous.add_permission! :add_issues, :edit_issues - users = Project.find(1).users - tester = users.first - - # Issue form - get '/projects/ecookbook/issues/new' - assert_response :success - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][#{@field.id}]"}, - :children => {:count => (users.size + 1)}, # +1 for blank value - :child => { - :tag => 'option', - :attributes => {:value => tester.id.to_s}, - :content => tester.name - } - - # Create issue - assert_difference 'Issue.count' do - post '/projects/ecookbook/issues', - :issue => { - :tracker_id => '1', - :priority_id => '4', - :subject => 'Issue with user custom field', - :custom_field_values => {@field.id.to_s => users.first.id.to_s} - } - end - issue = Issue.first(:order => 'id DESC') - assert_response 302 - - # Issue view - follow_redirect! - assert_tag :th, - :content => /Tester/, - :sibling => { - :tag => 'td', - :content => tester.name - } - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][#{@field.id}]"}, - :children => {:count => (users.size + 1)}, # +1 for blank value - :child => { - :tag => 'option', - :attributes => {:value => tester.id.to_s, :selected => 'selected'}, - :content => tester.name - } - - # Update issue - new_tester = users[1] - assert_difference 'Journal.count' do - put "/issues/#{issue.id}", - :notes => 'Updating custom field', - :issue => { - :custom_field_values => {@field.id.to_s => new_tester.id.to_s} - } - end - assert_response 302 - - # Issue view - follow_redirect! - assert_tag :content => 'Tester', - :ancestor => {:tag => 'ul', :attributes => {:class => /details/}}, - :sibling => { - :content => tester.name, - :sibling => { - :content => new_tester.name - } - } - end - - def test_update_using_invalid_http_verbs - subject = 'Updated by an invalid http verb' - - get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith') - assert_response 404 - assert_not_equal subject, Issue.find(1).subject - - post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith') - assert_response 404 - assert_not_equal subject, Issue.find(1).subject - end - - def test_get_watch_should_be_invalid - assert_no_difference 'Watcher.count' do - get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith') - assert_response 404 - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d90914cb67314627792a9dd72c779cc666b81ba.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d90914cb67314627792a9dd72c779cc666b81ba.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,40 @@ +<%= wiki_page_breadcrumb(@page) %> + +<%= title [@page.pretty_title, project_wiki_page_path(@page.project, @page.title, :version => nil)], l(:label_history) %> + +<%= form_tag({:controller => 'wiki', :action => 'diff', + :project_id => @page.project, :id => @page.title}, + :method => :get) do %> + + + + + + + + + + + +<% show_diff = @versions.size > 1 %> +<% line_num = 1 %> +<% @versions.each do |ver| %> +"> + + + + + + + + +<% line_num += 1 %> +<% end %> + +
    #<%= l(:field_updated_on) %><%= l(:field_author) %><%= l(:field_comments) %>
    <%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').attr('checked', true);") if show_diff && (line_num < @versions.size) %><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %><%= format_time(ver.updated_on) %><%= link_to_user ver.author %><%=h ver.comments %> + <%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %> + <%= delete_link wiki_page_path(@page, :version => ver.version) if User.current.allowed_to?(:delete_wiki_pages, @page.project) && @version_count > 1 %> +
    +<%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %> +<%= pagination_links_full @version_pages, @version_count %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2d90db0331b1140b778e4338d292ab96881523cf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d90db0331b1140b778e4338d292ab96881523cf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,110 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::IssueCategoriesTest < Redmine::ApiTest::Base + fixtures :projects, :users, :issue_categories, :issues, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + end + + test "GET /projects/:project_id/issue_categories.xml should return the issue categories" do + get '/projects/1/issue_categories.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_categories', + :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}} + end + + test "GET /issue_categories/:id.xml should return the issue category" do + get '/issue_categories/2.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_category', + :child => {:tag => 'id', :content => '2'} + end + + test "POST /projects/:project_id/issue_categories.xml should return create issue category" do + assert_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + category = IssueCategory.order('id DESC').first + assert_equal 'API', category.name + assert_equal 1, category.project_id + end + + test "POST /projects/:project_id/issue_categories.xml with invalid parameters should return errors" do + assert_no_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + + test "PUT /issue_categories/:id.xml with valid parameters should update the issue category" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'API Update', IssueCategory.find(2).name + end + + test "PUT /issue_categories/:id.xml with invalid parameters should return errors" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + + test "DELETE /issue_categories/:id.xml should destroy the issue category" do + assert_difference 'IssueCategory.count', -1 do + delete '/issue_categories/1.xml', {}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end + + test "DELETE /issue_categories/:id.xml should reassign issues with :reassign_to_id param" do + issue_count = Issue.where(:category_id => 1).count + assert issue_count > 0 + + assert_difference 'IssueCategory.count', -1 do + assert_difference 'Issue.where(:category_id => 2).count', 3 do + delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith') + end + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2d/2df87670ca6521ac395806c3506ecdba3eeb973c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2df87670ca6521ac395806c3506ecdba3eeb973c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,54 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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.group('status').count.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 d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2e1157d3f81f3880c0395bb75f0b975ce97e7bd4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e1157d3f81f3880c0395bb75f0b975ce97e7bd4.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,8 @@ +<%= error_messages_for @group %> + +
    +

    <%= f.text_field :name, :required => true, :size => 60 %>

    + <% @group.custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :group, value %>

    + <% end %> +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2e2d614b9e64145af9606e9c3e56e58eb315056d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e2d614b9e64145af9606e9c3e56e58eb315056d.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,34 @@ +
    +
    "> + <%= l(:label_filter_plural) %> +
    "> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
    +
    + +
    + +

    + <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %> +

    + +
    +<% query_params = params.slice(:f, :op, :v, :sort) %> +
      +
    • <%= link_to(l(:label_details), query_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }), + :class => (action_name == 'index' ? 'selected' : nil)) %>
    • +
    • <%= link_to(l(:label_report), query_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}), + :class => (action_name == 'report' ? 'selected' : nil)) %>
    • +
    +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2e6c87fbf008e52f19138f6c4bd2e546325437b5.svn-base --- a/.svn/pristine/2e/2e6c87fbf008e52f19138f6c4bd2e546325437b5.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -<% if issues && issues.any? %> -<%= form_tag({}) do %> - - - - - - - - - <% for issue in issues %> - - - - - - - <% end %> - -
    #<%=l(:field_project)%><%=l(:field_tracker)%><%=l(:field_subject)%>
    - <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %> - <%= link_to(h(issue.id), :controller => 'issues', :action => 'show', :id => issue) %> - <%= link_to_project(issue.project) %><%=h issue.tracker %> - <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>) -
    -<% end %> -<% else %> -

    <%= l(:label_no_data) %>

    -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2e761f17a6eb6248759d869c76abad698e943338.svn-base --- a/.svn/pristine/2e/2e761f17a6eb6248759d869c76abad698e943338.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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, - :workflows, - :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_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 d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2e7adb230b6d192e3aff1f060927ae3a68396262.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e7adb230b6d192e3aff1f060927ae3a68396262.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,43 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 WikiHelper + + def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0) + pages = pages.group_by(&:parent) unless pages.is_a?(Hash) + s = ''.html_safe + if pages.has_key?(parent) + pages[parent].each do |page| + attrs = "value='#{page.id}'" + attrs << " selected='selected'" if selected == page + indent = (level > 0) ? (' ' * level * 2 + '» ') : '' + + s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) + + wiki_page_options_for_select(pages, selected, page, level + 1) + end + end + s + end + + def wiki_page_breadcrumb(page) + breadcrumb(page.ancestors.reverse.collect {|parent| + link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil}) + }) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2e8224806b7f3ca405d76c48dfaa7adc74623a47.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e8224806b7f3ca405d76c48dfaa7adc74623a47.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,37 @@ +module Redmine + module Info + class << self + def app_name; 'Redmine' end + def url; 'http://www.redmine.org/' end + def help_url; 'http://www.redmine.org/guide' end + def versioned_name; "#{app_name} #{Redmine::VERSION}" end + + def environment + s = "Environment:\n" + s << [ + ["Redmine version", Redmine::VERSION], + ["Ruby version", "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"], + ["Rails version", Rails::VERSION::STRING], + ["Environment", Rails.env], + ["Database adapter", ActiveRecord::Base.connection.adapter_name] + ].map {|info| " %-30s %s" % info}.join("\n") + "\n" + + s << "SCM:\n" + Redmine::Scm::Base.all.each do |scm| + scm_class = "Repository::#{scm}".constantize + if scm_class.scm_available + s << " %-30s %s\n" % [scm, scm_class.scm_version_string] + end + end + + s << "Redmine plugins:\n" + plugins = Redmine::Plugin.all + if plugins.any? + s << plugins.map {|plugin| " %-30s %s" % [plugin.id.to_s, plugin.version.to_s]}.join("\n") + else + s << " no plugin installed" + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2e/2eb44b7abed16dd8cc469c08dde7704eb259a823.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2eb44b7abed16dd8cc469c08dde7704eb259a823.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,122 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2f281714ddeccdbd5c86760f52d4c587260d33ae.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2f281714ddeccdbd5c86760f52d4c587260d33ae.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,33 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RoutingWikisTest < ActionController::IntegrationTest + def test_wikis_plural_admin_setup + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/projects/ladida/wiki/destroy" }, + { :controller => 'wikis', :action => 'destroy', :id => 'ladida' } + ) + end + assert_routing( + { :method => 'post', :path => "/projects/ladida/wiki" }, + { :controller => 'wikis', :action => 'edit', :id => 'ladida' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2f285623343d12f6ac399f9ec52ab70933439f61.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2f285623343d12f6ac399f9ec52ab70933439f61.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,484 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueQuery < Query + + self.queried_class = Issue + + self.available_columns = [ + QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true), + QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), + QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), + QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue), + QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true), + QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), + QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), + QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true), + QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), + QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), + QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), + QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true), + QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), + QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), + QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), + QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), + QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), + QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), + QueryColumn.new(:relations, :caption => :label_related_issues), + QueryColumn.new(:description, :inline => false) + ] + + scope :visible, lambda {|*args| + user = args.shift || User.current + base = Project.allowed_to_condition(user, :view_issues, *args) + scope = includes(:project).where("#{table_name}.project_id IS NULL OR (#{base})") + + if user.admin? + scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id) + elsif user.memberships.any? + scope.where("#{table_name}.visibility = ?" + + " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" + + "SELECT DISTINCT q.id FROM #{table_name} q" + + " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" + + " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" + + " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" + + " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" + + " OR #{table_name}.user_id = ?", + VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id) + elsif user.logged? + scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id) + else + scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC) + end + } + + def initialize(attributes=nil, *args) + super attributes + self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } + end + + # Returns true if the query is visible to +user+ or the current user. + def visible?(user=User.current) + return true if user.admin? + return false unless project.nil? || user.allowed_to?(:view_issues, project) + case visibility + when VISIBILITY_PUBLIC + true + when VISIBILITY_ROLES + if project + (user.roles_for_project(project) & roles).any? + else + Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any? + end + else + user == self.user + end + end + + def is_private? + visibility == VISIBILITY_PRIVATE + end + + def is_public? + !is_private? + end + + def draw_relations + r = options[:draw_relations] + r.nil? || r == '1' + end + + def draw_relations=(arg) + options[:draw_relations] = (arg == '0' ? '0' : nil) + end + + def draw_progress_line + r = options[:draw_progress_line] + r == '1' + end + + def draw_progress_line=(arg) + options[:draw_progress_line] = (arg == '1' ? '1' : nil) + end + + def build_from_params(params) + super + self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations]) + self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line]) + self + end + + def initialize_available_filters + principals = [] + subprojects = [] + versions = [] + categories = [] + issue_custom_fields = [] + + if project + principals += project.principals.sort + unless project.leaf? + subprojects = project.descendants.visible.all + principals += Principal.member_of(subprojects) + end + versions = project.shared_versions.all + categories = project.issue_categories.all + issue_custom_fields = project.all_issue_custom_fields + else + if all_projects.any? + principals += Principal.member_of(all_projects) + end + versions = Version.visible.where(:sharing => 'system').all + issue_custom_fields = IssueCustomField.where(:is_for_all => true) + end + principals.uniq! + principals.sort! + users = principals.select {|p| p.is_a?(User)} + + add_available_filter "status_id", + :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } + + if project.nil? + project_values = [] + if User.current.logged? && User.current.memberships.any? + project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] + end + project_values += all_projects_values + add_available_filter("project_id", + :type => :list, :values => project_values + ) unless project_values.empty? + end + + add_available_filter "tracker_id", + :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] } + add_available_filter "priority_id", + :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } + + author_values = [] + author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + author_values += users.collect{|s| [s.name, s.id.to_s] } + add_available_filter("author_id", + :type => :list, :values => author_values + ) unless author_values.empty? + + assigned_to_values = [] + assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + assigned_to_values += (Setting.issue_group_assignment? ? + principals : users).collect{|s| [s.name, s.id.to_s] } + add_available_filter("assigned_to_id", + :type => :list_optional, :values => assigned_to_values + ) unless assigned_to_values.empty? + + group_values = Group.all.collect {|g| [g.name, g.id.to_s] } + add_available_filter("member_of_group", + :type => :list_optional, :values => group_values + ) unless group_values.empty? + + role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } + add_available_filter("assigned_to_role", + :type => :list_optional, :values => role_values + ) unless role_values.empty? + + if versions.any? + add_available_filter "fixed_version_id", + :type => :list_optional, + :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } + end + + if categories.any? + add_available_filter "category_id", + :type => :list_optional, + :values => categories.collect{|s| [s.name, s.id.to_s] } + end + + add_available_filter "subject", :type => :text + add_available_filter "created_on", :type => :date_past + add_available_filter "updated_on", :type => :date_past + add_available_filter "closed_on", :type => :date_past + add_available_filter "start_date", :type => :date + add_available_filter "due_date", :type => :date + add_available_filter "estimated_hours", :type => :float + add_available_filter "done_ratio", :type => :integer + + if User.current.allowed_to?(:set_issues_private, nil, :global => true) || + User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + add_available_filter "is_private", + :type => :list, + :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] + end + + if User.current.logged? + add_available_filter "watcher_id", + :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]] + end + + if subprojects.any? + add_available_filter "subproject_id", + :type => :list_subprojects, + :values => subprojects.collect{|s| [s.name, s.id.to_s] } + end + + add_custom_fields_filters(issue_custom_fields) + + add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version + + IssueRelation::TYPES.each do |relation_type, options| + add_available_filter relation_type, :type => :relation, :label => options[:name] + end + + Tracker.disabled_core_fields(trackers).each {|field| + delete_available_filter field + } + end + + def available_columns + return @available_columns if @available_columns + @available_columns = self.class.available_columns.dup + @available_columns += (project ? + project.all_issue_custom_fields : + IssueCustomField + ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) } + + if User.current.allowed_to?(:view_time_entries, project, :global => true) + index = nil + @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} + index = (index ? index + 1 : -1) + # insert the column after estimated_hours or at the end + @available_columns.insert index, QueryColumn.new(:spent_hours, + :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)", + :default_order => 'desc', + :caption => :label_spent_time + ) + end + + if User.current.allowed_to?(:set_issues_private, nil, :global => true) || + User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private") + end + + disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} + @available_columns.reject! {|column| + disabled_fields.include?(column.name.to_s) + } + + @available_columns + end + + def default_columns_names + @default_columns_names ||= begin + default_columns = Setting.issue_list_default_columns.map(&:to_sym) + + project.present? ? default_columns : [:project] | default_columns + end + end + + # Returns the issue count + def issue_count + Issue.visible.joins(:status, :project).where(statement).count + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issue count by group or nil if query is not grouped + def issue_count_by_group + r = nil + if grouped? + begin + # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value + r = Issue.visible. + joins(:status, :project). + where(statement). + joins(joins_for_order_statement(group_by_statement)). + group(group_by_statement). + count + rescue ActiveRecord::RecordNotFound + r = {nil => issue_count} + end + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issues + # Valid options are :order, :offset, :limit, :include, :conditions + def issues(options={}) + order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) + + scope = Issue.visible. + joins(:status, :project). + where(statement). + includes(([:status, :project] + (options[:include] || [])).uniq). + where(options[:conditions]). + order(order_option). + joins(joins_for_order_statement(order_option.join(','))). + limit(options[:limit]). + offset(options[:offset]) + + scope = scope.preload(:custom_values) + if has_column?(:author) + scope = scope.preload(:author) + end + + issues = scope.all + + if has_column?(:spent_hours) + Issue.load_visible_spent_hours(issues) + end + if has_column?(:relations) + Issue.load_visible_relations(issues) + end + issues + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issues ids + def issue_ids(options={}) + order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) + + Issue.visible. + joins(:status, :project). + where(statement). + includes(([:status, :project] + (options[:include] || [])).uniq). + where(options[:conditions]). + order(order_option). + joins(joins_for_order_statement(order_option.join(','))). + limit(options[:limit]). + offset(options[:offset]). + find_ids + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the journals + # Valid options are :order, :offset, :limit + def journals(options={}) + Journal.visible. + joins(:issue => [:project, :status]). + where(statement). + order(options[:order]). + limit(options[:limit]). + offset(options[:offset]). + preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}). + all + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the versions + # Valid options are :conditions + def versions(options={}) + Version.visible. + where(project_statement). + where(options[:conditions]). + includes(:project). + all + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def sql_for_watcher_id_field(field, operator, value) + db_table = Watcher.table_name + "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " + + sql_for_field(field, '=', value, db_table, 'user_id') + ')' + end + + def sql_for_member_of_group_field(field, operator, value) + if operator == '*' # Any group + groups = Group.all + operator = '=' # Override the operator since we want to find by assigned_to + elsif operator == "!*" + groups = Group.all + operator = '!' # Override the operator since we want to find by assigned_to + else + groups = Group.where(:id => value).all + end + groups ||= [] + + members_of_groups = groups.inject([]) {|user_ids, group| + user_ids + group.user_ids + [group.id] + }.uniq.compact.sort.collect(&:to_s) + + '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' + end + + def sql_for_assigned_to_role_field(field, operator, value) + case operator + when "*", "!*" # Member / Not member + sw = operator == "!*" ? 'NOT' : '' + nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' + "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + + " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" + when "=", "!" + role_cond = value.any? ? + "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : + "1=0" + + sw = operator == "!" ? 'NOT' : '' + nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' + "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" + + " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))" + end + end + + def sql_for_is_private_field(field, operator, value) + op = (operator == "=" ? 'IN' : 'NOT IN') + va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',') + + "#{Issue.table_name}.is_private #{op} (#{va})" + end + + def sql_for_relations(field, operator, value, options={}) + relation_options = IssueRelation::TYPES[field] + return relation_options unless relation_options + + relation_type = field + join_column, target_join_column = "issue_from_id", "issue_to_id" + if relation_options[:reverse] || options[:reverse] + relation_type = relation_options[:reverse] || relation_type + join_column, target_join_column = target_join_column, join_column + end + + sql = case operator + when "*", "!*" + op = (operator == "*" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')" + when "=", "!" + op = (operator == "=" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})" + when "=p", "=!p", "!p" + op = (operator == "!p" ? 'NOT IN' : 'IN') + comp = (operator == "=!p" ? '<>' : '=') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})" + end + + if relation_options[:sym] == field && !options[:reverse] + sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] + sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") + end + "(#{sql})" + end + + IssueRelation::TYPES.keys.each do |relation_type| + alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2f42ecc608d36e4b4728b04529751c42ed317e4f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2f42ecc608d36e4b4728b04529751c42ed317e4f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldVersionFormatTest < ActiveSupport::TestCase + fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues, :versions + + def setup + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'version') + end + + def test_possible_values_with_no_arguments + assert_equal [], @field.possible_values + assert_equal [], @field.possible_values(nil) + end + + def test_possible_values_with_project_resource + project = Project.find(1) + possible_values = @field.possible_values(project.issues.first) + assert possible_values.any? + assert_equal project.shared_versions.sort.collect(&:id).map(&:to_s), possible_values + end + + def test_possible_values_with_nil_project_resource + assert_equal [], @field.possible_values(Issue.new) + end + + def test_possible_values_options_with_no_arguments + assert_equal [], @field.possible_values_options + assert_equal [], @field.possible_values_options(nil) + end + + def test_possible_values_options_with_project_resource + project = Project.find(1) + possible_values_options = @field.possible_values_options(project.issues.first) + assert possible_values_options.any? + assert_equal project.shared_versions.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_possible_values_options_with_array + projects = Project.find([1, 2]) + possible_values_options = @field.possible_values_options(projects) + assert possible_values_options.any? + assert_equal (projects.first.shared_versions & projects.last.shared_versions).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_cast_blank_value + assert_equal nil, @field.cast_value(nil) + assert_equal nil, @field.cast_value("") + end + + def test_cast_valid_value + version = @field.cast_value("2") + assert_kind_of Version, version + assert_equal Version.find(2), version + end + + def test_cast_invalid_value + assert_equal nil, @field.cast_value("187") + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2f549bc1e936040c7191cdf4cf1d592cb479c086.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2f549bc1e936040c7191cdf4cf1d592cb479c086.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,141 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 AuthSourceLdapTest < ActiveSupport::TestCase + include Redmine::I18n + fixtures :auth_sources + + def setup + end + + def test_create + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName') + assert a.save + end + + def test_should_strip_ldap_attributes + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', + :attr_firstname => 'givenName ') + assert a.save + assert_equal 'givenName', a.reload.attr_firstname + end + + def test_replace_port_zero_to_389 + a = AuthSourceLdap.new( + :name => 'My LDAP', :host => 'ldap.example.net', :port => 0, + :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', + :attr_firstname => 'givenName ') + assert a.save + assert_equal 389, a.port + end + + def test_filter_should_be_validated + set_language_if_valid 'en' + + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn') + a.filter = "(mail=*@redmine.org" + assert !a.valid? + assert_include "LDAP filter is invalid", a.errors.full_messages + + a.filter = "(mail=*@redmine.org)" + assert a.valid? + end + + if ldap_configured? + test '#authenticate with a valid LDAP user should return the user attributes' do + auth = AuthSourceLdap.find(1) + auth.update_attribute :onthefly_register, true + + attributes = auth.authenticate('example1','123456') + assert attributes.is_a?(Hash), "An hash was not returned" + assert_equal 'Example', attributes[:firstname] + assert_equal 'One', attributes[:lastname] + assert_equal 'example1@redmine.org', attributes[:mail] + assert_equal auth.id, attributes[:auth_source_id] + attributes.keys.each do |attribute| + assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned" + end + end + + test '#authenticate with an invalid LDAP user should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('nouser','123456') + end + + test '#authenticate without a login should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('','123456') + end + + test '#authenticate without a password should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('edavis','') + end + + test '#authenticate without filter should return any user' do + auth = AuthSourceLdap.find(1) + assert auth.authenticate('example1','123456') + assert auth.authenticate('edavis', '123456') + end + + test '#authenticate with filter should return user who matches the filter only' do + auth = AuthSourceLdap.find(1) + auth.filter = "(mail=*@redmine.org)" + + assert auth.authenticate('example1','123456') + assert_nil auth.authenticate('edavis', '123456') + end + + def test_authenticate_should_timeout + auth_source = AuthSourceLdap.find(1) + auth_source.timeout = 1 + def auth_source.initialize_ldap_con(*args); sleep(5); end + + assert_raise AuthSourceTimeoutException do + auth_source.authenticate 'example1', '123456' + end + end + + def test_search_should_return_matching_entries + results = AuthSource.search("exa") + assert_equal 1, results.size + result = results.first + assert_kind_of Hash, result + assert_equal "example1", result[:login] + assert_equal "Example", result[:firstname] + assert_equal "One", result[:lastname] + assert_equal "example1@redmine.org", result[:mail] + assert_equal 1, result[:auth_source_id] + end + + def test_search_with_no_match_should_return_an_empty_array + results = AuthSource.search("wro") + assert_equal [], results + end + + def test_search_with_exception_should_return_an_empty_array + Net::LDAP.stubs(:new).raises(Net::LDAP::LdapError, 'Cannot connect') + + results = AuthSource.search("exa") + assert_equal [], results + end + else + puts '(Test LDAP server not configured)' + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2f84c0e762bf35955a1559bdc6fbe56124b4f457.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2f84c0e762bf35955a1559bdc6fbe56124b4f457.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,256 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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_a_news_module_should_add_watcher + @request.session[:user_id] = 7 + assert_not_nil m = Project.find(1).enabled_module('news') + + assert_difference 'Watcher.count' do + xhr :post, :watch, :object_type => 'enabled_module', :object_id => m.id.to_s + assert_response :success + end + assert m.reload.watched_by?(User.find(7)) + end + + def test_watch_a_private_news_module_without_permission_should_fail + @request.session[:user_id] = 7 + assert_not_nil m = Project.find(2).enabled_module('news') + + assert_no_difference 'Watcher.count' do + xhr :post, :watch, :object_type => 'enabled_module', :object_id => m.id.to_s + assert_response 403 + end + 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_search_non_member_on_create + @request.session[:user_id] = 2 + project = Project.find_by_name("ecookbook") + user = User.generate!(:firstname => 'issue15622') + membership = user.membership(project) + assert_nil membership + xhr :get, :autocomplete_for_user, :q => 'issue15622', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 1 + 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_search_and_add_non_member_on_update + @request.session[:user_id] = 2 + project = Project.find_by_name("ecookbook") + user = User.generate!(:firstname => 'issue15622') + membership = user.membership(project) + assert_nil membership + xhr :get, :autocomplete_for_user, :q => 'issue15622', :object_id => '2', + :object_type => 'issue', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 1 + assert_difference('Watcher.count', 1) do + xhr :post, :create, :object_type => 'issue', :object_id => '2', + :watcher => {:user_ids => ["#{user.id}"]} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(user) + 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_append_without_user_should_render_nothing + @request.session[:user_id] = 2 + xhr :post, :append, :project_id => 'ecookbook' + assert_response :success + assert response.body.blank? + 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 d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2f968415d6662b8ce52a48cbeb3f56b87c5a9e9f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2f968415d6662b8ce52a48cbeb3f56b87c5a9e9f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,118 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 Jean-Philippe Lang +# +# FileSystem adapter +# File written by Paul Rivier, at Demotera. +# +# 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/abstract_adapter' +require 'find' + +module Redmine + module Scm + module Adapters + class FilesystemAdapter < AbstractAdapter + + class << self + def client_available + true + end + end + + def initialize(url, root_url=nil, login=nil, password=nil, + path_encoding=nil) + @url = with_trailling_slash(url) + @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding + end + + def path_encoding + @path_encoding + end + + def format_path_ends(path, leading=true, trailling=true) + path = leading ? with_leading_slash(path) : + without_leading_slash(path) + trailling ? with_trailling_slash(path) : + without_trailling_slash(path) + end + + def info + info = Info.new({:root_url => target(), + :lastrev => nil + }) + info + rescue CommandFailed + return nil + end + + def entries(path="", identifier=nil, options={}) + entries = Entries.new + trgt_utf8 = target(path) + trgt = scm_iconv(@path_encoding, 'UTF-8', trgt_utf8) + Dir.new(trgt).each do |e1| + e_utf8 = scm_iconv('UTF-8', @path_encoding, e1) + next if e_utf8.blank? + relative_path_utf8 = format_path_ends( + (format_path_ends(path,false,true) + e_utf8),false,false) + t1_utf8 = target(relative_path_utf8) + t1 = scm_iconv(@path_encoding, 'UTF-8', t1_utf8) + relative_path = scm_iconv(@path_encoding, 'UTF-8', relative_path_utf8) + e1 = scm_iconv(@path_encoding, 'UTF-8', e_utf8) + if File.exist?(t1) and # paranoid test + %w{file directory}.include?(File.ftype(t1)) and # avoid special types + not File.basename(e1).match(/^\.+$/) # avoid . and .. + p1 = File.readable?(t1) ? relative_path : "" + utf_8_path = scm_iconv('UTF-8', @path_encoding, p1) + entries << + Entry.new({ :name => scm_iconv('UTF-8', @path_encoding, File.basename(e1)), + # below : list unreadable files, but dont link them. + :path => utf_8_path, + :kind => (File.directory?(t1) ? 'dir' : 'file'), + :size => (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first), + :lastrev => + Revision.new({:time => (File.mtime(t1)) }) + }) + end + end + entries.sort_by_name + rescue => err + logger.error "scm: filesystem: error: #{err.message}" + raise CommandFailed.new(err.message) + end + + def cat(path, identifier=nil) + p = scm_iconv(@path_encoding, 'UTF-8', target(path)) + File.new(p, "rb").read + rescue => err + logger.error "scm: filesystem: error: #{err.message}" + raise CommandFailed.new(err.message) + end + + private + + # AbstractAdapter::target is implicitly made to quote paths. + # Here we do not shell-out, so we do not want quotes. + def target(path=nil) + # Prevent the use of .. + if path and !path.match(/(^|\/)\.\.(\/|$)/) + return "#{self.url}#{without_leading_slash(path)}" + end + return self.url + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/2f/2fb6efca6ea9d063a58d2593af2f43422f15665e.svn-base --- a/.svn/pristine/2f/2fb6efca6ea9d063a58d2593af2f43422f15665e.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,279 +0,0 @@ -module ActiveRecord - module Acts #:nodoc: - module List #:nodoc: - def self.included(base) - base.extend(ClassMethods) - end - - # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list. - # The class that has this specified needs to have a +position+ column defined as an integer on - # the mapped database table. - # - # Todo list example: - # - # class TodoList < ActiveRecord::Base - # has_many :todo_items, :order => "position" - # end - # - # class TodoItem < ActiveRecord::Base - # belongs_to :todo_list - # acts_as_list :scope => :todo_list - # end - # - # todo_list.first.move_to_bottom - # todo_list.last.move_higher - module ClassMethods - # Configuration options are: - # - # * +column+ - specifies the column name to use for keeping the position integer (default: +position+) - # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id - # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible - # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. - # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' - def acts_as_list(options = {}) - configuration = { :column => "position", :scope => "1 = 1" } - configuration.update(options) if options.is_a?(Hash) - - configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ - - if configuration[:scope].is_a?(Symbol) - scope_condition_method = %( - def scope_condition - if #{configuration[:scope].to_s}.nil? - "#{configuration[:scope].to_s} IS NULL" - else - "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" - end - end - ) - else - scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" - end - - class_eval <<-EOV - include ActiveRecord::Acts::List::InstanceMethods - - def acts_as_list_class - ::#{self.name} - end - - def position_column - '#{configuration[:column]}' - end - - #{scope_condition_method} - - before_destroy :remove_from_list - before_create :add_to_list_bottom - EOV - end - end - - # All the methods available to a record that has had acts_as_list specified. Each method works - # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter - # lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is - # the first in the list of all chapters. - module InstanceMethods - # Insert the item at the given position (defaults to the top position of 1). - def insert_at(position = 1) - insert_at_position(position) - end - - # Swap positions with the next lower item, if one exists. - def move_lower - return unless lower_item - - acts_as_list_class.transaction do - lower_item.decrement_position - increment_position - end - end - - # Swap positions with the next higher item, if one exists. - def move_higher - return unless higher_item - - acts_as_list_class.transaction do - higher_item.increment_position - decrement_position - end - end - - # Move to the bottom of the list. If the item is already in the list, the items below it have their - # position adjusted accordingly. - def move_to_bottom - return unless in_list? - acts_as_list_class.transaction do - decrement_positions_on_lower_items - assume_bottom_position - end - end - - # Move to the top of the list. If the item is already in the list, the items above it have their - # position adjusted accordingly. - def move_to_top - return unless in_list? - acts_as_list_class.transaction do - increment_positions_on_higher_items - assume_top_position - end - end - - # Move to the given position - def move_to=(pos) - case pos.to_s - when 'highest' - move_to_top - when 'higher' - move_higher - when 'lower' - move_lower - when 'lowest' - move_to_bottom - end - reset_positions_in_list - end - - def reset_positions_in_list - acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i| - unless item.send(position_column) == (i + 1) - acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id}) - end - end - end - - # Removes the item from the list. - def remove_from_list - if in_list? - decrement_positions_on_lower_items - update_attribute position_column, nil - end - end - - # Increase the position of this item without adjusting the rest of the list. - def increment_position - return unless in_list? - update_attribute position_column, self.send(position_column).to_i + 1 - end - - # Decrease the position of this item without adjusting the rest of the list. - def decrement_position - return unless in_list? - update_attribute position_column, self.send(position_column).to_i - 1 - end - - # Return +true+ if this object is the first in the list. - def first? - return false unless in_list? - self.send(position_column) == 1 - end - - # Return +true+ if this object is the last in the list. - def last? - return false unless in_list? - self.send(position_column) == bottom_position_in_list - end - - # Return the next higher item in the list. - def higher_item - return nil unless in_list? - acts_as_list_class.find(:first, :conditions => - "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" - ) - end - - # Return the next lower item in the list. - def lower_item - return nil unless in_list? - acts_as_list_class.find(:first, :conditions => - "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" - ) - end - - # Test if this record is in a list - def in_list? - !send(position_column).nil? - end - - private - def add_to_list_top - increment_positions_on_all_items - end - - def add_to_list_bottom - self[position_column] = bottom_position_in_list.to_i + 1 - end - - # Overwrite this method to define the scope of the list changes - def scope_condition() "1" end - - # Returns the bottom position number in the list. - # bottom_position_in_list # => 2 - def bottom_position_in_list(except = nil) - item = bottom_item(except) - item ? item.send(position_column) : 0 - end - - # Returns the bottom item - def bottom_item(except = nil) - conditions = scope_condition - conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except - acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first - end - - # Forces item to assume the bottom position in the list. - def assume_bottom_position - update_attribute(position_column, bottom_position_in_list(self).to_i + 1) - end - - # Forces item to assume the top position in the list. - def assume_top_position - update_attribute(position_column, 1) - end - - # This has the effect of moving all the higher items up one. - def decrement_positions_on_higher_items(position) - acts_as_list_class.update_all( - "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}" - ) - end - - # This has the effect of moving all the lower items up one. - def decrement_positions_on_lower_items - return unless in_list? - acts_as_list_class.update_all( - "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}" - ) - end - - # This has the effect of moving all the higher items down one. - def increment_positions_on_higher_items - return unless in_list? - acts_as_list_class.update_all( - "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}" - ) - end - - # This has the effect of moving all the lower items down one. - def increment_positions_on_lower_items(position) - acts_as_list_class.update_all( - "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" - ) - end - - # Increments position (position_column) of all items in the list. - def increment_positions_on_all_items - acts_as_list_class.update_all( - "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" - ) - end - - def insert_at_position(position) - remove_from_list - increment_positions_on_lower_items(position) - self.update_attribute(position_column, position) - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/30/3023c43eca89098e7ad3d81f2db133b0643baf81.svn-base --- a/.svn/pristine/30/3023c43eca89098e7ad3d81f2db133b0643baf81.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,134 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 'redcloth3' -require 'digest/md5' - -module Redmine - module WikiFormatting - module Textile - class Formatter < RedCloth3 - include ActionView::Helpers::TagHelper - include Redmine::WikiFormatting::LinksHelper - - alias :inline_auto_link :auto_link! - alias :inline_auto_mailto :auto_mailto! - - # auto_link rule after textile rules so that it doesn't break !image_url! tags - RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] - - def initialize(*args) - super - self.hard_breaks=true - self.no_span_caps=true - self.filter_styles=false - end - - def to_html(*rules) - @toc = [] - super(*RULES).to_s - end - - def get_section(index) - section = extract_sections(index)[1] - hash = Digest::MD5.hexdigest(section) - return section, hash - end - - def update_section(index, update, hash=nil) - t = extract_sections(index) - if hash.present? && hash != Digest::MD5.hexdigest(t[1]) - raise Redmine::WikiFormatting::StaleSectionError - end - t[1] = update unless t[1].blank? - t.reject(&:blank?).join "\n\n" - end - - def extract_sections(index) - @pre_list = [] - text = self.dup - rip_offtags text, false, false - before = '' - s = '' - after = '' - i = 0 - l = 1 - started = false - ended = false - text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level| - if heading.nil? - if ended - after << all - elsif started - s << all - else - before << all - end - break - end - i += 1 - if ended - after << all - elsif i == index - l = level.to_i - before << content - s << heading - started = true - elsif i > index - s << content - if level.to_i > l - s << heading - else - after << heading - ended = true - end - else - before << all - end - end - sections = [before.strip, s.strip, after.strip] - sections.each {|section| smooth_offtags_without_code_highlighting section} - sections - end - - private - - # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. - # http://code.whytheluckystiff.net/redcloth/changeset/128 - def hard_break( text ) - text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks - end - - alias :smooth_offtags_without_code_highlighting :smooth_offtags - # Patch to add code highlighting support to RedCloth - def smooth_offtags( text ) - unless @pre_list.empty? - ## replace
     content
    -            text.gsub!(//) do
    -              content = @pre_list[$1.to_i]
    -              if content.match(/\s?(.+)/m)
    -                content = "" +
    -                  Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
    -              end
    -              content
    -            end
    -          end
    -        end
    -      end
    -    end
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/30/3027fca91424b88657f6c91d9b6b5ddfc109c616.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/3027fca91424b88657f6c91d9b6b5ddfc109c616.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,37 @@
    +require 'rexml/document'
    +
    +module Redmine
    +  module VERSION #:nodoc:
    +    MAJOR = 2
    +    MINOR = 4
    +    TINY  = 6
    +
    +    # 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 d98d22a98252 -r fb9a13467253 .svn/pristine/30/3036a2491d22fe798c178f3e275812ed2e72870c.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/3036a2491d22fe798c178f3e275812ed2e72870c.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,90 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2014  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 SettingTest < ActiveSupport::TestCase
    +
    +  def teardown
    +    Setting.clear_cache
    +  end
    +
    +  def test_read_default
    +    assert_equal "Redmine", Setting.app_title
    +    assert Setting.self_registration?
    +    assert !Setting.login_required?
    +  end
    +
    +  def test_update
    +    Setting.app_title = "My title"
    +    assert_equal "My title", Setting.app_title
    +    # make sure db has been updated (INSERT)
    +    assert_equal "My title", Setting.find_by_name('app_title').value
    +
    +    Setting.app_title = "My other title"
    +    assert_equal "My other title", Setting.app_title
    +    # make sure db has been updated (UPDATE)
    +    assert_equal "My other title", Setting.find_by_name('app_title').value
    +  end
    +
    +  def test_serialized_setting
    +    Setting.notified_events = ['issue_added', 'issue_updated', 'news_added']
    +    assert_equal ['issue_added', 'issue_updated', 'news_added'], Setting.notified_events
    +    assert_equal ['issue_added', 'issue_updated', 'news_added'], Setting.find_by_name('notified_events').value
    +  end
    +
    +  def test_setting_should_be_reloaded_after_clear_cache
    +    Setting.app_title = "My title"
    +    assert_equal "My title", Setting.app_title
    +
    +    s = Setting.find_by_name("app_title")
    +    s.value = 'New title'
    +    s.save!
    +    assert_equal "My title", Setting.app_title
    +
    +    Setting.clear_cache
    +    assert_equal "New title", Setting.app_title
    +  end
    +
    +  def test_per_page_options_array_should_be_an_empty_array_when_setting_is_blank
    +    with_settings :per_page_options => nil do
    +      assert_equal [], Setting.per_page_options_array
    +    end
    +
    +    with_settings :per_page_options => '' do
    +      assert_equal [], Setting.per_page_options_array
    +    end
    +  end
    +
    +  def test_per_page_options_array_should_be_an_array_of_integers
    +    with_settings :per_page_options => '10, 25, 50' do
    +      assert_equal [10, 25, 50], Setting.per_page_options_array
    +    end
    +  end
    +
    +  def test_per_page_options_array_should_omit_non_numerial_values
    +    with_settings :per_page_options => 'a, 25, 50' do
    +      assert_equal [25, 50], Setting.per_page_options_array
    +    end
    +  end
    +
    +  def test_per_page_options_array_should_be_sorted
    +    with_settings :per_page_options => '25, 10, 50' do
    +      assert_equal [10, 25, 50], Setting.per_page_options_array
    +    end
    +  end
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/30/30a02acbacee457029ab442e649789febcfd2f94.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/30a02acbacee457029ab442e649789febcfd2f94.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,36 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2014  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 WikisController < ApplicationController
    +  menu_item :settings
    +  before_filter :find_project, :authorize
    +
    +  # Create or update a project's wiki
    +  def edit
    +    @wiki = @project.wiki || Wiki.new(:project => @project)
    +    @wiki.safe_attributes = params[:wiki]
    +    @wiki.save if request.post?
    +  end
    +
    +  # Delete a project's wiki
    +  def destroy
    +    if request.post? && params[:confirm] && @project.wiki
    +      @project.wiki.destroy
    +      redirect_to settings_project_path(@project, :tab => 'wiki')
    +    end
    +  end
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/30/30accb20c8e9d14a6bba9ac1e15c6b0323bdc2b1.svn-base
    --- a/.svn/pristine/30/30accb20c8e9d14a6bba9ac1e15c6b0323bdc2b1.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,35 +0,0 @@
    -# Redmine - project management software
    -# Copyright (C) 2006-2012  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 :db do
    -  desc 'Encrypts SCM and LDAP passwords in the database.'
    -  task :encrypt => :environment do
    -    unless (Repository.encrypt_all(:password) &&
    -      AuthSource.encrypt_all(:account_password))
    -      raise "Some objects could not be saved after encryption, update was rollback'ed."
    -    end
    -  end
    -
    -  desc 'Decrypts SCM and LDAP passwords in the database.'
    -  task :decrypt => :environment do
    -    unless (Repository.decrypt_all(:password) &&
    -      AuthSource.decrypt_all(:account_password))
    -      raise "Some objects could not be saved after decryption, update was rollback'ed."
    -    end
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/30/30b1a4d0c6108cec480ef030683c6821bc105094.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/30b1a4d0c6108cec480ef030683c6821bc105094.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,39 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2014  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 EnabledModule < ActiveRecord::Base
    +  belongs_to :project
    +  acts_as_watchable
    +
    +  validates_presence_of :name
    +  validates_uniqueness_of :name, :scope => :project_id
    +
    +  after_create :module_enabled
    +
    +  private
    +
    +  # after_create callback used to do things when a module is enabled
    +  def module_enabled
    +    case name
    +    when 'wiki'
    +      # Create a wiki with a default start page
    +      if project && project.wiki.nil?
    +        Wiki.create(:project => project, :start_page => 'Wiki')
    +      end
    +    end
    +  end
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/3153fac39abb0847d15ae1b313574c54e0570e1c.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/31/3153fac39abb0847d15ae1b313574c54e0570e1c.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,37 @@
    +api.user do
    +  api.id         @user.id
    +  api.login      @user.login if User.current.admin? || (User.current == @user)
    +  api.firstname  @user.firstname
    +  api.lastname   @user.lastname
    +  api.mail       @user.mail if User.current.admin? || !@user.pref.hide_mail
    +  api.created_on @user.created_on
    +  api.last_login_on @user.last_login_on
    +  api.api_key    @user.api_key if User.current.admin? || (User.current == @user)
    +  api.status     @user.status if User.current.admin?
    +
    +  render_api_custom_values @user.visible_custom_field_values, api
    +
    +  api.array :groups do |groups|
    +    @user.groups.each do |group|
    +      api.group :id => group.id, :name => group.name
    +    end
    +  end if User.current.admin? && include_in_api_response?('groups')
    +
    +  api.array :memberships do
    +    @memberships.each do |membership|
    +      api.membership do
    +        api.id membership.id
    +        api.project :id => membership.project.id, :name => membership.project.name
    +        api.array :roles do
    +          membership.member_roles.each do |member_role|
    +            if member_role.role
    +              attrs = {:id => member_role.role.id, :name => member_role.role.name}
    +              attrs.merge!(:inherited => true) if member_role.inherited_from.present?
    +              api.role attrs
    +            end 
    +          end
    +        end
    +      end if membership.project
    +    end
    +  end if include_in_api_response?('memberships') && @memberships
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/3155f8295657e15516d6d855aaf7eefbf749a747.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/31/3155f8295657e15516d6d855aaf7eefbf749a747.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,195 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2014  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 d98d22a98252 -r fb9a13467253 .svn/pristine/31/318f04133e3e32c333e98ac7f8065761353f5042.svn-base
    --- a/.svn/pristine/31/318f04133e3e32c333e98ac7f8065761353f5042.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,10 +0,0 @@
    -class AddIssueStatusPosition < ActiveRecord::Migration
    -  def self.up
    -    add_column :issue_statuses, :position, :integer, :default => 1
    -    IssueStatus.find(:all).each_with_index {|status, i| status.update_attribute(:position, i+1)}
    -  end
    -
    -  def self.down
    -    remove_column :issue_statuses, :position
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/3190ee598f70a34c3d3f93dfd554a0ad83b8e462.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/31/3190ee598f70a34c3d3f93dfd554a0ad83b8e462.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,637 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2014  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 'uri'
    +require 'cgi'
    +
    +class Unauthorized < Exception; end
    +
    +class ApplicationController < ActionController::Base
    +  include Redmine::I18n
    +  include Redmine::Pagination
    +  include RoutesHelper
    +  helper :routes
    +
    +  class_attribute :accept_api_auth_actions
    +  class_attribute :accept_rss_auth_actions
    +  class_attribute :model_object
    +
    +  layout 'base'
    +
    +  protect_from_forgery
    +
    +  def verify_authenticity_token
    +    unless api_request?
    +      super
    +    end
    +  end
    +
    +  def handle_unverified_request
    +    unless api_request?
    +      super
    +      cookies.delete(autologin_cookie_name)
    +      self.logged_user = nil
    +      render_error :status => 422, :message => "Invalid form authenticity token."
    +    end
    +  end
    +
    +  before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
    +
    +  rescue_from ::Unauthorized, :with => :deny_access
    +  rescue_from ::ActionView::MissingTemplate, :with => :missing_template
    +
    +  include Redmine::Search::Controller
    +  include Redmine::MenuManager::MenuController
    +  helper Redmine::MenuManager::MenuHelper
    +
    +  def session_expiration
    +    if session[:user_id]
    +      if session_expired? && !try_to_autologin
    +        reset_session
    +        flash[:error] = l(:error_session_expired)
    +        redirect_to signin_url
    +      else
    +        session[:atime] = Time.now.utc.to_i
    +      end
    +    end
    +  end
    +
    +  def session_expired?
    +    if Setting.session_lifetime?
    +      unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
    +        return true
    +      end
    +    end
    +    if Setting.session_timeout?
    +      unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
    +        return true
    +      end
    +    end
    +    false
    +  end
    +
    +  def start_user_session(user)
    +    session[:user_id] = user.id
    +    session[:ctime] = Time.now.utc.to_i
    +    session[:atime] = Time.now.utc.to_i
    +    if user.must_change_password?
    +      session[:pwd] = '1'
    +    end
    +  end
    +
    +  def user_setup
    +    # Check the settings cache for each request
    +    Setting.check_cache
    +    # Find the current user
    +    User.current = find_current_user
    +    logger.info("  Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
    +  end
    +
    +  # Returns the current user or nil if no user is logged in
    +  # and starts a session if needed
    +  def find_current_user
    +    user = nil
    +    unless api_request?
    +      if session[:user_id]
    +        # existing session
    +        user = (User.active.find(session[:user_id]) rescue nil)
    +      elsif autologin_user = try_to_autologin
    +        user = autologin_user
    +      elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
    +        # RSS key authentication does not start a session
    +        user = User.find_by_rss_key(params[:key])
    +      end
    +    end
    +    if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
    +      if (key = api_key_from_request)
    +        # Use API key
    +        user = User.find_by_api_key(key)
    +      else
    +        # HTTP Basic, either username/password or API key/random
    +        authenticate_with_http_basic do |username, password|
    +          user = User.try_to_login(username, password) || User.find_by_api_key(username)
    +        end
    +        if user && user.must_change_password?
    +          render_error :message => 'You must change your password', :status => 403
    +          return
    +        end
    +      end
    +      # Switch user if requested by an admin user
    +      if user && user.admin? && (username = api_switch_user_from_request)
    +        su = User.find_by_login(username)
    +        if su && su.active?
    +          logger.info("  User switched by: #{user.login} (id=#{user.id})") if logger
    +          user = su
    +        else
    +          render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
    +        end
    +      end
    +    end
    +    user
    +  end
    +
    +  def autologin_cookie_name
    +    Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
    +  end
    +
    +  def try_to_autologin
    +    if cookies[autologin_cookie_name] && Setting.autologin?
    +      # auto-login feature starts a new session
    +      user = User.try_to_autologin(cookies[autologin_cookie_name])
    +      if user
    +        reset_session
    +        start_user_session(user)
    +      end
    +      user
    +    end
    +  end
    +
    +  # Sets the logged in user
    +  def logged_user=(user)
    +    reset_session
    +    if user && user.is_a?(User)
    +      User.current = user
    +      start_user_session(user)
    +    else
    +      User.current = User.anonymous
    +    end
    +  end
    +
    +  # Logs out current user
    +  def logout_user
    +    if User.current.logged?
    +      cookies.delete(autologin_cookie_name)
    +      Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
    +      self.logged_user = nil
    +    end
    +  end
    +
    +  # check if login is globally required to access the application
    +  def check_if_login_required
    +    # no check needed if user is already logged in
    +    return true if User.current.logged?
    +    require_login if Setting.login_required?
    +  end
    +
    +  def check_password_change
    +    if session[:pwd]
    +      if User.current.must_change_password?
    +        redirect_to my_password_path
    +      else
    +        session.delete(:pwd)
    +      end
    +    end
    +  end
    +
    +  def set_localization
    +    lang = nil
    +    if User.current.logged?
    +      lang = find_language(User.current.language)
    +    end
    +    if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
    +      accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
    +      if !accept_lang.blank?
    +        accept_lang = accept_lang.downcase
    +        lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
    +      end
    +    end
    +    lang ||= Setting.default_language
    +    set_language_if_valid(lang)
    +  end
    +
    +  def require_login
    +    if !User.current.logged?
    +      # Extract only the basic url parameters on non-GET requests
    +      if request.get?
    +        url = url_for(params)
    +      else
    +        url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
    +      end
    +      respond_to do |format|
    +        format.html {
    +          if request.xhr?
    +            head :unauthorized
    +          else
    +            redirect_to :controller => "account", :action => "login", :back_url => url
    +          end
    +        }
    +        format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
    +        format.xml  { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
    +        format.js   { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
    +        format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
    +      end
    +      return false
    +    end
    +    true
    +  end
    +
    +  def require_admin
    +    return unless require_login
    +    if !User.current.admin?
    +      render_403
    +      return false
    +    end
    +    true
    +  end
    +
    +  def deny_access
    +    User.current.logged? ? render_403 : require_login
    +  end
    +
    +  # Authorize the user for the requested action
    +  def authorize(ctrl = params[:controller], action = params[:action], global = false)
    +    allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
    +    if allowed
    +      true
    +    else
    +      if @project && @project.archived?
    +        render_403 :message => :notice_not_authorized_archived_project
    +      else
    +        deny_access
    +      end
    +    end
    +  end
    +
    +  # Authorize the user for the requested action outside a project
    +  def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
    +    authorize(ctrl, action, global)
    +  end
    +
    +  # Find project of id params[:id]
    +  def find_project
    +    @project = Project.find(params[:id])
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    +
    +  # Find project of id params[:project_id]
    +  def find_project_by_project_id
    +    @project = Project.find(params[:project_id])
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    +
    +  # Find a project based on params[:project_id]
    +  # TODO: some subclasses override this, see about merging their logic
    +  def find_optional_project
    +    @project = Project.find(params[:project_id]) unless params[:project_id].blank?
    +    allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
    +    allowed ? true : deny_access
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    +
    +  # Finds and sets @project based on @object.project
    +  def find_project_from_association
    +    render_404 unless @object.present?
    +
    +    @project = @object.project
    +  end
    +
    +  def find_model_object
    +    model = self.class.model_object
    +    if model
    +      @object = model.find(params[:id])
    +      self.instance_variable_set('@' + controller_name.singularize, @object) if @object
    +    end
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    +
    +  def self.model_object(model)
    +    self.model_object = model
    +  end
    +
    +  # Find the issue whose id is the :id parameter
    +  # Raises a Unauthorized exception if the issue is not visible
    +  def find_issue
    +    # Issue.visible.find(...) can not be used to redirect user to the login form
    +    # if the issue actually exists but requires authentication
    +    @issue = Issue.find(params[:id])
    +    raise Unauthorized unless @issue.visible?
    +    @project = @issue.project
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    +
    +  # Find issues with a single :id param or :ids array param
    +  # Raises a Unauthorized exception if one of the issues is not visible
    +  def find_issues
    +    @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
    +    raise ActiveRecord::RecordNotFound if @issues.empty?
    +    raise Unauthorized unless @issues.all?(&:visible?)
    +    @projects = @issues.collect(&:project).compact.uniq
    +    @project = @projects.first if @projects.size == 1
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    +
    +  def find_attachments
    +    if (attachments = params[:attachments]).present?
    +      att = attachments.values.collect do |attachment|
    +        Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
    +      end
    +      att.compact!
    +    end
    +    @attachments = att || []
    +  end
    +
    +  # make sure that the user is a member of the project (or admin) if project is private
    +  # used as a before_filter for actions that do not require any particular permission on the project
    +  def check_project_privacy
    +    if @project && !@project.archived?
    +      if @project.visible?
    +        true
    +      else
    +        deny_access
    +      end
    +    else
    +      @project = nil
    +      render_404
    +      false
    +    end
    +  end
    +
    +  def back_url
    +    url = params[:back_url]
    +    if url.nil? && referer = request.env['HTTP_REFERER']
    +      url = CGI.unescape(referer.to_s)
    +    end
    +    url
    +  end
    +
    +  def redirect_back_or_default(default)
    +    back_url = params[:back_url].to_s
    +    if back_url.present? && valid_back_url?(back_url)
    +      redirect_to(back_url)
    +      return
    +    end
    +    redirect_to default
    +    false
    +  end
    +
    +  # Returns true if back_url is a valid url for redirection, otherwise false
    +  def valid_back_url?(back_url)
    +    if CGI.unescape(back_url).include?('..')
    +      return false
    +    end
    +
    +    begin
    +      uri = URI.parse(back_url)
    +    rescue URI::InvalidURIError
    +      return false
    +    end
    +
    +    if uri.host.present? && uri.host != request.host
    +      return false
    +    end
    +
    +    if uri.path.match(%r{/(login|account/register)})
    +      return false
    +    end
    +
    +    if relative_url_root.present? && !uri.path.starts_with?(relative_url_root)
    +      return false
    +    end
    +
    +    return true
    +  end
    +  private :valid_back_url?
    +
    +  # Redirects to the request referer if present, redirects to args or call block otherwise.
    +  def redirect_to_referer_or(*args, &block)
    +    redirect_to :back
    +  rescue ::ActionController::RedirectBackError
    +    if args.any?
    +      redirect_to *args
    +    elsif block_given?
    +      block.call
    +    else
    +      raise "#redirect_to_referer_or takes arguments or a block"
    +    end
    +  end
    +
    +  def render_403(options={})
    +    @project = nil
    +    render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
    +    return false
    +  end
    +
    +  def render_404(options={})
    +    render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
    +    return false
    +  end
    +
    +  # Renders an error response
    +  def render_error(arg)
    +    arg = {:message => arg} unless arg.is_a?(Hash)
    +
    +    @message = arg[:message]
    +    @message = l(@message) if @message.is_a?(Symbol)
    +    @status = arg[:status] || 500
    +
    +    respond_to do |format|
    +      format.html {
    +        render :template => 'common/error', :layout => use_layout, :status => @status
    +      }
    +      format.any { head @status }
    +    end
    +  end
    +
    +  # Handler for ActionView::MissingTemplate exception
    +  def missing_template
    +    logger.warn "Missing template, responding with 404"
    +    @project = nil
    +    render_404
    +  end
    +
    +  # Filter for actions that provide an API response
    +  # but have no HTML representation for non admin users
    +  def require_admin_or_api_request
    +    return true if api_request?
    +    if User.current.admin?
    +      true
    +    elsif User.current.logged?
    +      render_error(:status => 406)
    +    else
    +      deny_access
    +    end
    +  end
    +
    +  # Picks which layout to use based on the request
    +  #
    +  # @return [boolean, string] name of the layout to use or false for no layout
    +  def use_layout
    +    request.xhr? ? false : 'base'
    +  end
    +
    +  def render_feed(items, options={})
    +    @items = items || []
    +    @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
    +    @items = @items.slice(0, Setting.feeds_limit.to_i)
    +    @title = options[:title] || Setting.app_title
    +    render :template => "common/feed", :formats => [:atom], :layout => false,
    +           :content_type => 'application/atom+xml'
    +  end
    +
    +  def self.accept_rss_auth(*actions)
    +    if actions.any?
    +      self.accept_rss_auth_actions = actions
    +    else
    +      self.accept_rss_auth_actions || []
    +    end
    +  end
    +
    +  def accept_rss_auth?(action=action_name)
    +    self.class.accept_rss_auth.include?(action.to_sym)
    +  end
    +
    +  def self.accept_api_auth(*actions)
    +    if actions.any?
    +      self.accept_api_auth_actions = actions
    +    else
    +      self.accept_api_auth_actions || []
    +    end
    +  end
    +
    +  def accept_api_auth?(action=action_name)
    +    self.class.accept_api_auth.include?(action.to_sym)
    +  end
    +
    +  # Returns the number of objects that should be displayed
    +  # on the paginated list
    +  def per_page_option
    +    per_page = nil
    +    if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
    +      per_page = params[:per_page].to_s.to_i
    +      session[:per_page] = per_page
    +    elsif session[:per_page]
    +      per_page = session[:per_page]
    +    else
    +      per_page = Setting.per_page_options_array.first || 25
    +    end
    +    per_page
    +  end
    +
    +  # Returns offset and limit used to retrieve objects
    +  # for an API response based on offset, limit and page parameters
    +  def api_offset_and_limit(options=params)
    +    if options[:offset].present?
    +      offset = options[:offset].to_i
    +      if offset < 0
    +        offset = 0
    +      end
    +    end
    +    limit = options[:limit].to_i
    +    if limit < 1
    +      limit = 25
    +    elsif limit > 100
    +      limit = 100
    +    end
    +    if offset.nil? && options[:page].present?
    +      offset = (options[:page].to_i - 1) * limit
    +      offset = 0 if offset < 0
    +    end
    +    offset ||= 0
    +
    +    [offset, limit]
    +  end
    +
    +  # qvalues http header parser
    +  # code taken from webrick
    +  def parse_qvalues(value)
    +    tmp = []
    +    if value
    +      parts = value.split(/,\s*/)
    +      parts.each {|part|
    +        if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
    +          val = m[1]
    +          q = (m[2] or 1).to_f
    +          tmp.push([val, q])
    +        end
    +      }
    +      tmp = tmp.sort_by{|val, q| -q}
    +      tmp.collect!{|val, q| val}
    +    end
    +    return tmp
    +  rescue
    +    nil
    +  end
    +
    +  # Returns a string that can be used as filename value in Content-Disposition header
    +  def filename_for_content_disposition(name)
    +    request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
    +  end
    +
    +  def api_request?
    +    %w(xml json).include? params[:format]
    +  end
    +
    +  # Returns the API key present in the request
    +  def api_key_from_request
    +    if params[:key].present?
    +      params[:key].to_s
    +    elsif request.headers["X-Redmine-API-Key"].present?
    +      request.headers["X-Redmine-API-Key"].to_s
    +    end
    +  end
    +
    +  # Returns the API 'switch user' value if present
    +  def api_switch_user_from_request
    +    request.headers["X-Redmine-Switch-User"].to_s.presence
    +  end
    +
    +  # Renders a warning flash if obj has unsaved attachments
    +  def render_attachment_warning_if_needed(obj)
    +    flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
    +  end
    +
    +  # Rescues an invalid query statement. Just in case...
    +  def query_statement_invalid(exception)
    +    logger.error "Query::StatementInvalid: #{exception.message}" if logger
    +    session.delete(:query)
    +    sort_clear if respond_to?(:sort_clear)
    +    render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
    +  end
    +
    +  # Renders a 200 response for successfull updates or deletions via the API
    +  def render_api_ok
    +    render_api_head :ok
    +  end
    +
    +  # Renders a head API response
    +  def render_api_head(status)
    +    # #head would return a response body with one space
    +    render :text => '', :status => status, :layout => nil
    +  end
    +
    +  # Renders API response on validation failure
    +  def render_validation_errors(objects)
    +    if objects.is_a?(Array)
    +      @error_messages = objects.map {|object| object.errors.full_messages}.flatten
    +    else
    +      @error_messages = objects.errors.full_messages
    +    end
    +    render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
    +  end
    +
    +  # Overrides #_include_layout? so that #render with no arguments
    +  # doesn't use the layout for api requests
    +  def _include_layout?(*args)
    +    api_request? ? false : super
    +  end
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/31bb85f22f4d21a57af6d104f857fd83f936b8f3.svn-base
    --- a/.svn/pristine/31/31bb85f22f4d21a57af6d104f857fd83f936b8f3.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,65 +0,0 @@
    -# Redmine - project management software
    -# Copyright (C) 2006-2012  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 WorkflowTest < ActiveSupport::TestCase
    -  fixtures :roles, :trackers, :issue_statuses
    -
    -  def test_copy
    -    WorkflowTransition.delete_all
    -    WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 2)
    -    WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 3, :assignee => true)
    -    WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 4, :author => true)
    -
    -    assert_difference 'WorkflowTransition.count', 3 do
    -      WorkflowTransition.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2))
    -    end
    -
    -    assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false})
    -    assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true})
    -    assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false})
    -  end
    -
    -  def test_workflow_permission_should_validate_rule
    -    wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :field_name => 'due_date')
    -    assert !wp.save
    -
    -    wp.rule = 'foo'
    -    assert !wp.save
    -
    -    wp.rule = 'required'
    -    assert wp.save
    -
    -    wp.rule = 'readonly'
    -    assert wp.save
    -  end
    -
    -  def test_workflow_permission_should_validate_field_name
    -    wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :rule => 'required')
    -    assert !wp.save
    -
    -    wp.field_name = 'foo'
    -    assert !wp.save
    -
    -    wp.field_name = 'due_date'
    -    assert wp.save
    -
    -    wp.field_name = '1'
    -    assert wp.save
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/31dde3fcb99bae1f1bf825ebba05c5ceb4525424.svn-base
    --- a/.svn/pristine/31/31dde3fcb99bae1f1bf825ebba05c5ceb4525424.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,27 +0,0 @@
    -
    -<%= link_to_if_authorized l(:label_query_new), new_project_query_path(:project_id => @project), :class => 'icon icon-add' %> -
    - -

    <%= l(:label_query_plural) %>

    - -<% if @queries.empty? %> -

    <%=l(:label_no_data)%>

    -<% else %> - - <% @queries.each do |query| %> - - - - - <% end %> -
    - <%= link_to h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %> - - - <% if query.editable_by?(User.current) %> - <%= link_to l(:button_edit), edit_query_path(query), :class => 'icon icon-edit' %> - <%= delete_link query_path(query) %> - - <% end %> -
    -<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/31eee5aeaf6a5a394428efea4fd82e6e1daac3a9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/31/31eee5aeaf6a5a394428efea4fd82e6e1daac3a9.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,106 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::CipheringTest < ActiveSupport::TestCase + + def test_password_should_be_encrypted + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'svn') + assert_equal 'foo', r.password + assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/) + end + end + + def test_password_should_be_clear_with_blank_key + Redmine::Configuration.with 'database_cipher_key' => '' do + r = Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'svn') + assert_equal 'foo', r.password + assert_equal 'foo', r.read_attribute(:password) + end + end + + def test_password_should_be_clear_with_nil_key + Redmine::Configuration.with 'database_cipher_key' => nil do + r = Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'svn') + assert_equal 'foo', r.password + assert_equal 'foo', r.read_attribute(:password) + end + end + + def test_blank_password_should_be_clear + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository::Subversion.create!(:password => '', :url => 'file:///tmp', :identifier => 'svn') + assert_equal '', r.password + assert_equal '', r.read_attribute(:password) + end + end + + def test_unciphered_password_should_be_readable + Redmine::Configuration.with 'database_cipher_key' => nil do + r = Repository::Subversion.create!(:password => 'clear', :url => 'file:///tmp', :identifier => 'svn') + end + + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository.first(:order => 'id DESC') + assert_equal 'clear', r.password + end + end + + def test_ciphered_password_with_no_cipher_key_configured_should_be_returned_ciphered + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository::Subversion.create!(:password => 'clear', :url => 'file:///tmp', :identifier => 'svn') + end + + Redmine::Configuration.with 'database_cipher_key' => '' do + r = Repository.first(:order => 'id DESC') + # password can not be deciphered + assert_nothing_raised do + assert r.password.match(/\Aaes-256-cbc:.+\Z/) + end + end + end + + def test_encrypt_all + Repository.delete_all + Redmine::Configuration.with 'database_cipher_key' => nil do + Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'foo') + Repository::Subversion.create!(:password => 'bar', :url => 'file:///tmp', :identifier => 'bar') + end + + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + assert Repository.encrypt_all(:password) + r = Repository.first(:order => 'id DESC') + assert_equal 'bar', r.password + assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/) + end + end + + def test_decrypt_all + Repository.delete_all + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'foo') + Repository::Subversion.create!(:password => 'bar', :url => 'file:///tmp', :identifier => 'bar') + + assert Repository.decrypt_all(:password) + r = Repository.first(:order => 'id DESC') + assert_equal 'bar', r.password + assert_equal 'bar', r.read_attribute(:password) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/31/31f8831dd1cf0c2ef63a3b5e3c999a2d0f32707b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/31/31f8831dd1cf0c2ef63a3b5e3c999a2d0f32707b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,134 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'redcloth3' +require 'digest/md5' + +module Redmine + module WikiFormatting + module Textile + class Formatter < RedCloth3 + include ActionView::Helpers::TagHelper + include Redmine::WikiFormatting::LinksHelper + + alias :inline_auto_link :auto_link! + alias :inline_auto_mailto :auto_mailto! + + # auto_link rule after textile rules so that it doesn't break !image_url! tags + RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] + + def initialize(*args) + super + self.hard_breaks=true + self.no_span_caps=true + self.filter_styles=false + end + + def to_html(*rules) + @toc = [] + super(*RULES).to_s + end + + def get_section(index) + section = extract_sections(index)[1] + hash = Digest::MD5.hexdigest(section) + return section, hash + end + + def update_section(index, update, hash=nil) + t = extract_sections(index) + if hash.present? && hash != Digest::MD5.hexdigest(t[1]) + raise Redmine::WikiFormatting::StaleSectionError + end + t[1] = update unless t[1].blank? + t.reject(&:blank?).join "\n\n" + end + + def extract_sections(index) + @pre_list = [] + text = self.dup + rip_offtags text, false, false + before = '' + s = '' + after = '' + i = 0 + l = 1 + started = false + ended = false + text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level| + if heading.nil? + if ended + after << all + elsif started + s << all + else + before << all + end + break + end + i += 1 + if ended + after << all + elsif i == index + l = level.to_i + before << content + s << heading + started = true + elsif i > index + s << content + if level.to_i > l + s << heading + else + after << heading + ended = true + end + else + before << all + end + end + sections = [before.strip, s.strip, after.strip] + sections.each {|section| smooth_offtags_without_code_highlighting section} + sections + end + + private + + # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. + # http://code.whytheluckystiff.net/redcloth/changeset/128 + def hard_break( text ) + text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks + end + + alias :smooth_offtags_without_code_highlighting :smooth_offtags + # Patch to add code highlighting support to RedCloth + def smooth_offtags( text ) + unless @pre_list.empty? + ## replace
     content
    +            text.gsub!(//) do
    +              content = @pre_list[$1.to_i]
    +              if content.match(/\s?(.+)/m)
    +                content = "" +
    +                  Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
    +              end
    +              content
    +            end
    +          end
    +        end
    +      end
    +    end
    +  end
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/323c5888f7f213199068cdb2446ac990b6701da1.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/32/323c5888f7f213199068cdb2446ac990b6701da1.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,89 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2014  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 RepositoryFilesystemTest < ActiveSupport::TestCase
    +  fixtures :projects
    +
    +  include Redmine::I18n
    +
    +  REPOSITORY_PATH = Rails.root.join('tmp/test/filesystem_repository').to_s
    +
    +  def setup
    +    @project = Project.find(3)
    +    Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem')
    +    @repository = Repository::Filesystem.create(
    +                               :project => @project,
    +                               :url     => REPOSITORY_PATH
    +                                 )
    +    assert @repository
    +  end
    +
    +  def test_blank_root_directory_error_message
    +    set_language_if_valid 'en'
    +    repo = Repository::Filesystem.new(
    +                          :project      => @project,
    +                          :identifier   => 'test'
    +                        )
    +    assert !repo.save
    +    assert_include "Root directory can't be blank",
    +                   repo.errors.full_messages
    +  end
    +
    +  def test_blank_root_directory_error_message_fr
    +    set_language_if_valid 'fr'
    +    str = "R\xc3\xa9pertoire racine doit \xc3\xaatre renseign\xc3\xa9(e)"
    +    str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
    +    repo = Repository::Filesystem.new(
    +                          :project      => @project,
    +                          :url          => "",
    +                          :identifier   => 'test',
    +                          :path_encoding => ''
    +                        )
    +    assert !repo.save
    +    assert_include str, repo.errors.full_messages
    +  end
    +
    +  if File.directory?(REPOSITORY_PATH)
    +    def test_fetch_changesets
    +      assert_equal 0, @repository.changesets.count
    +      assert_equal 0, @repository.filechanges.count
    +      @repository.fetch_changesets
    +      @project.reload
    +      assert_equal 0, @repository.changesets.count
    +      assert_equal 0, @repository.filechanges.count
    +    end
    +
    +    def test_entries
    +      entries = @repository.entries("", 2)
    +      assert_kind_of Redmine::Scm::Adapters::Entries, entries
    +      assert_equal 3, entries.size
    +    end
    +
    +    def test_entries_in_directory
    +      assert_equal 2, @repository.entries("dir", 3).size
    +    end
    +
    +    def test_cat
    +      assert_equal "TEST CAT\n", @repository.scm.cat("test")
    +    end
    +  else
    +    puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
    +    def test_fake; assert true end
    +  end
    +end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/325cbd5f2592da1a1293c4a50be14ad1338348eb.svn-base
    --- a/.svn/pristine/32/325cbd5f2592da1a1293c4a50be14ad1338348eb.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,212 +0,0 @@
    -# Redmine - project management software
    -# Copyright (C) 2006-2012  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 ApiTest::GroupsTest < ActionController::IntegrationTest
    -  fixtures :users, :groups_users
    -
    -  def setup
    -    Setting.rest_api_enabled = '1'
    -  end
    -
    -  context "GET /groups" do
    -    context ".xml" do
    -      should "require authentication" do
    -        get '/groups.xml'
    -        assert_response 401
    -      end
    -
    -      should "return groups" do
    -        get '/groups.xml', {}, credentials('admin')
    -        assert_response :success
    -        assert_equal 'application/xml', response.content_type
    -
    -        assert_select 'groups' do
    -          assert_select 'group' do
    -            assert_select 'name', :text => 'A Team'
    -            assert_select 'id', :text => '10'
    -          end
    -        end
    -      end
    -    end
    -
    -    context ".json" do
    -      should "require authentication" do
    -        get '/groups.json'
    -        assert_response 401
    -      end
    -
    -      should "return groups" do
    -        get '/groups.json', {}, credentials('admin')
    -        assert_response :success
    -        assert_equal 'application/json', response.content_type
    -
    -        json = MultiJson.load(response.body)
    -        groups = json['groups']
    -        assert_kind_of Array, groups
    -        group = groups.detect {|g| g['name'] == 'A Team'}
    -        assert_not_nil group
    -        assert_equal({'id' => 10, 'name' => 'A Team'}, group)
    -      end
    -    end
    -  end
    -
    -  context "GET /groups/:id" do
    -    context ".xml" do
    -      should "return the group with its users" do
    -        get '/groups/10.xml', {}, credentials('admin')
    -        assert_response :success
    -        assert_equal 'application/xml', response.content_type
    -
    -        assert_select 'group' do
    -          assert_select 'name', :text => 'A Team'
    -          assert_select 'id', :text => '10'
    -        end
    -      end
    -
    -      should "include users if requested" do
    -        get '/groups/10.xml?include=users', {}, credentials('admin')
    -        assert_response :success
    -        assert_equal 'application/xml', response.content_type
    -
    -        assert_select 'group' do
    -          assert_select 'users' do
    -            assert_select 'user', Group.find(10).users.count
    -            assert_select 'user[id=8]'
    -          end
    -        end
    -      end
    -
    -      should "include memberships if requested" do
    -        get '/groups/10.xml?include=memberships', {}, credentials('admin')
    -        assert_response :success
    -        assert_equal 'application/xml', response.content_type
    -
    -        assert_select 'group' do
    -          assert_select 'memberships'
    -        end
    -      end
    -    end
    -  end
    -
    -  context "POST /groups" do
    -    context "with valid parameters" do
    -      context ".xml" do
    -        should "create groups" do
    -          assert_difference('Group.count') do
    -            post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
    -            assert_response :created
    -            assert_equal 'application/xml', response.content_type
    -          end
    -  
    -          group = Group.order('id DESC').first
    -          assert_equal 'Test', group.name
    -          assert_equal [2, 3], group.users.map(&:id).sort
    -
    -          assert_select 'group' do
    -            assert_select 'name', :text => 'Test'
    -          end
    -        end
    -      end
    -    end
    -
    -    context "with invalid parameters" do
    -      context ".xml" do
    -        should "return errors" do
    -          assert_no_difference('Group.count') do
    -            post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
    -          end
    -          assert_response :unprocessable_entity
    -          assert_equal 'application/xml', response.content_type
    -
    -          assert_select 'errors' do
    -            assert_select 'error', :text => /Name can't be blank/
    -          end
    -        end
    -      end
    -    end
    -  end
    -
    -  context "PUT /groups/:id" do
    -    context "with valid parameters" do
    -      context ".xml" do
    -        should "update the group" do
    -          put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
    -          assert_response :ok
    -          assert_equal '', @response.body
    -  
    -          group = Group.find(10)
    -          assert_equal 'New name', group.name
    -          assert_equal [2, 3], group.users.map(&:id).sort
    -        end
    -      end
    -    end
    -
    -    context "with invalid parameters" do
    -      context ".xml" do
    -        should "return errors" do
    -          put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
    -          assert_response :unprocessable_entity
    -          assert_equal 'application/xml', response.content_type
    -
    -          assert_select 'errors' do
    -            assert_select 'error', :text => /Name can't be blank/
    -          end
    -        end
    -      end
    -    end
    -  end
    -
    -  context "DELETE /groups/:id" do
    -    context ".xml" do
    -      should "delete the group" do
    -        assert_difference 'Group.count', -1 do
    -          delete '/groups/10.xml', {}, credentials('admin')
    -          assert_response :ok
    -          assert_equal '', @response.body
    -        end
    -      end
    -    end
    -  end
    -
    -  context "POST /groups/:id/users" do
    -    context ".xml" do
    -      should "add user to the group" do
    -        assert_difference 'Group.find(10).users.count' do
    -          post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
    -          assert_response :ok
    -          assert_equal '', @response.body
    -        end
    -        assert_include User.find(5), Group.find(10).users
    -      end
    -    end
    -  end
    -
    -  context "DELETE /groups/:id/users/:user_id" do
    -    context ".xml" do
    -      should "remove user from the group" do
    -        assert_difference 'Group.find(10).users.count', -1 do
    -          delete '/groups/10/users/8.xml', {}, credentials('admin')
    -          assert_response :ok
    -          assert_equal '', @response.body
    -        end
    -        assert_not_include User.find(8), Group.find(10).users
    -      end
    -    end
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/3263175b802b3311f08d06f669648333f55d611c.svn-base
    --- a/.svn/pristine/32/3263175b802b3311f08d06f669648333f55d611c.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,51 +0,0 @@
    -# Redmine - project management software
    -# Copyright (C) 2006-2012  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 RoutingWatchersTest < ActionController::IntegrationTest
    -  def test_watchers
    -    assert_routing(
    -        { :method => 'get', :path => "/watchers/new" },
    -        { :controller => 'watchers', :action => 'new' }
    -      )
    -    assert_routing(
    -        { :method => 'post', :path => "/watchers/append" },
    -        { :controller => 'watchers', :action => 'append' }
    -      )
    -    assert_routing(
    -        { :method => 'post', :path => "/watchers" },
    -        { :controller => 'watchers', :action => 'create' }
    -      )
    -    assert_routing(
    -        { :method => 'post', :path => "/watchers/destroy" },
    -        { :controller => 'watchers', :action => 'destroy' }
    -      )
    -    assert_routing(
    -        { :method => 'get', :path => "/watchers/autocomplete_for_user" },
    -        { :controller => 'watchers', :action => 'autocomplete_for_user' }
    -      )
    -    assert_routing(
    -        { :method => 'post', :path => "/watchers/watch" },
    -        { :controller => 'watchers', :action => 'watch' }
    -      )
    -    assert_routing(
    -        { :method => 'post', :path => "/watchers/unwatch" },
    -        { :controller => 'watchers', :action => 'unwatch' }
    -      )
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/328e5d644f9c7f34ce3b37165d648bfc6611fce5.svn-base
    --- a/.svn/pristine/32/328e5d644f9c7f34ce3b37165d648bfc6611fce5.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,15 +0,0 @@
    -class AddCustomFieldsPosition < ActiveRecord::Migration
    -  def self.up
    -    add_column(:custom_fields, :position, :integer, :default => 1)
    -    CustomField.find(:all).group_by(&:type).each  do |t, fields|
    -      fields.each_with_index do |field, i|
    -        # do not call model callbacks
    -        CustomField.update_all "position = #{i+1}", {:id => field.id}
    -      end
    -    end
    -  end
    -
    -  def self.down
    -    remove_column :custom_fields, :position
    -  end
    -end
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/32b483f4b1d72e41eec78bed07706dfe97d3dda2.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/32/32b483f4b1d72e41eec78bed07706dfe97d3dda2.svn-base	Thu Sep 11 12:45:02 2014 +0100
    @@ -0,0 +1,41 @@
    +syntax: glob
    +
    +.project
    +.loadpath
    +.powrc
    +.rvmrc
    +config/additional_environment.rb
    +config/configuration.yml
    +config/database.yml
    +config/email.yml
    +config/initializers/session_store.rb
    +config/initializers/secret_token.rb
    +coverage
    +db/*.db
    +db/*.sqlite3
    +db/schema.rb
    +files/*
    +lib/redmine/scm/adapters/mercurial/redminehelper.pyc
    +lib/redmine/scm/adapters/mercurial/redminehelper.pyo
    +log/*.log*
    +log/mongrel_debug
    +public/dispatch.*
    +public/plugin_assets
    +tmp/*
    +tmp/cache/*
    +tmp/pdf/*
    +tmp/sessions/*
    +tmp/sockets/*
    +tmp/test/*
    +tmp/thumbnails/*
    +vendor/cache
    +vendor/rails
    +*.rbc
    +
    +.svn/
    +.git/
    +
    +.bundle
    +Gemfile.lock
    +Gemfile.local
    +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/32bb68107ce90e09885cb5aa647d1b1b670aefe9.svn-base
    --- a/.svn/pristine/32/32bb68107ce90e09885cb5aa647d1b1b670aefe9.svn-base	Wed May 07 14:15:02 2014 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,86 +0,0 @@
    -<%= board_breadcrumb(@message) %>
    -
    -
    - <%= watcher_tag(@topic, User.current) %> - <%= link_to( - l(:button_quote), - {:action => 'quote', :id => @topic}, - :remote => true, - :method => 'get', - :class => 'icon icon-comment', - :remote => true) if !@topic.locked? && authorize_for('messages', 'reply') %> - <%= link_to( - l(:button_edit), - {:action => 'edit', :id => @topic}, - :class => 'icon icon-edit' - ) if @message.editable_by?(User.current) %> - <%= link_to( - l(:button_delete), - {:action => 'destroy', :id => @topic}, - :method => :post, - :data => {:confirm => l(:text_are_you_sure)}, - :class => 'icon icon-del' - ) if @message.destroyable_by?(User.current) %> -
    - -

    <%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %>

    - -
    -

    <%= authoring @topic.created_on, @topic.author %>

    -
    -<%= textilizable(@topic, :content) %> -
    -<%= link_to_attachments @topic, :author => false %> -
    -
    - -<% unless @replies.empty? %> -

    <%= l(:label_reply_plural) %> (<%= @reply_count %>)

    -<% @replies.each do |message| %> -
    "> -
    - <%= link_to( - image_tag('comment.png'), - {:action => 'quote', :id => message}, - :remote => true, - :method => 'get', - :title => l(:button_quote)) if !@topic.locked? && authorize_for('messages', 'reply') %> - <%= link_to( - image_tag('edit.png'), - {:action => 'edit', :id => message}, - :title => l(:button_edit) - ) if message.editable_by?(User.current) %> - <%= link_to( - image_tag('delete.png'), - {:action => 'destroy', :id => message}, - :method => :post, - :data => {:confirm => l(:text_are_you_sure)}, - :title => l(:button_delete) - ) if message.destroyable_by?(User.current) %> -
    -

    - <%= avatar(message.author, :size => "24") %> - <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %> - - - <%= authoring message.created_on, message.author %> -

    -
    <%= textilizable message, :content, :attachments => message.attachments %>
    - <%= link_to_attachments message, :author => false %> -
    -<% end %> -

    <%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %>

    -<% end %> - -<% if !@topic.locked? && authorize_for('messages', 'reply') %> -

    <%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %>

    - -<% end %> - -<% html_title @topic.subject %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/32c52319e33e839952a511178f3d013ae8279f9b.svn-base --- a/.svn/pristine/32/32c52319e33e839952a511178f3d013ae8279f9b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 compute the start and end dates of a calendar - class Calendar - include Redmine::I18n - attr_reader :startdt, :enddt - - def initialize(date, lang = current_language, period = :month) - @date = date - @events = [] - @ending_events_by_days = {} - @starting_events_by_days = {} - set_language_if_valid lang - case period - when :month - @startdt = Date.civil(date.year, date.month, 1) - @enddt = (@startdt >> 1)-1 - # starts from the first day of the week - @startdt = @startdt - (@startdt.cwday - first_wday)%7 - # ends on the last day of the week - @enddt = @enddt + (last_wday - @enddt.cwday)%7 - when :week - @startdt = date - (date.cwday - first_wday)%7 - @enddt = date + (last_wday - date.cwday)%7 - else - raise 'Invalid period' - end - end - - # Sets calendar events - def events=(events) - @events = events - @ending_events_by_days = @events.group_by {|event| event.due_date} - @starting_events_by_days = @events.group_by {|event| event.start_date} - end - - # Returns events for the given day - def events_on(day) - ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq - end - - # Calendar current month - def month - @date.month - end - - # Return the first day of week - # 1 = Monday ... 7 = Sunday - def first_wday - case Setting.start_of_week.to_i - when 1 - @first_dow ||= (1 - 1)%7 + 1 - when 6 - @first_dow ||= (6 - 1)%7 + 1 - when 7 - @first_dow ||= (7 - 1)%7 + 1 - else - @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1 - end - end - - def last_wday - @last_dow ||= (first_wday + 5)%7 + 1 - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/32d36ad9d8096df93d00980c0d2786c39a657ed2.svn-base --- a/.svn/pristine/32/32d36ad9d8096df93d00980c0d2786c39a657ed2.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,247 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 WikiFormatting - module Macros - module Definitions - # Returns true if +name+ is the name of an existing macro - def macro_exists?(name) - Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym) - end - - def exec_macro(name, obj, args, text) - macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym] - return unless macro_options - - method_name = "macro_#{name}" - unless macro_options[:parse_args] == false - args = args.split(',').map(&:strip) - end - - begin - if self.class.instance_method(method_name).arity == 3 - send(method_name, obj, args, text) - elsif text - raise "This macro does not accept a block of text" - else - send(method_name, obj, args) - end - rescue => e - "
    Error executing the #{h name} macro (#{h e.to_s})
    ".html_safe - end - end - - def extract_macro_options(args, *keys) - options = {} - while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym) - options[$1.downcase.to_sym] = $2 - args.pop - end - return [args, options] - end - end - - @@available_macros = {} - mattr_accessor :available_macros - - class << self - # Plugins can use this method to define new macros: - # - # Redmine::WikiFormatting::Macros.register do - # desc "This is my macro" - # macro :my_macro do |obj, args| - # "My macro output" - # end - # - # desc "This is my macro that accepts a block of text" - # macro :my_macro do |obj, args, text| - # "My macro output" - # end - # end - def register(&block) - class_eval(&block) if block_given? - end - - # Defines a new macro with the given name, options and block. - # - # Options: - # * :desc - A description of the macro - # * :parse_args => false - Disables arguments parsing (the whole arguments - # string is passed to the macro) - # - # Macro blocks accept 2 or 3 arguments: - # * obj: the object that is rendered (eg. an Issue, a WikiContent...) - # * args: macro arguments - # * text: the block of text given to the macro (should be present only if the - # macro accepts a block of text). text is a String or nil if the macro is - # invoked without a block of text. - # - # Examples: - # By default, when the macro is invoked, the coma separated list of arguments - # is split and passed to the macro block as an array. If no argument is given - # the macro will be invoked with an empty array: - # - # macro :my_macro do |obj, args| - # # args is an array - # # and this macro do not accept a block of text - # end - # - # You can disable arguments spliting with the :parse_args => false option. In - # this case, the full string of arguments is passed to the macro: - # - # macro :my_macro, :parse_args => false do |obj, args| - # # args is a string - # end - # - # Macro can optionally accept a block of text: - # - # macro :my_macro do |obj, args, text| - # # this macro accepts a block of text - # end - # - # Macros are invoked in formatted text using double curly brackets. Arguments - # must be enclosed in parenthesis if any. A new line after the macro name or the - # arguments starts the block of text that will be passe to the macro (invoking - # a macro that do not accept a block of text with some text will fail). - # Examples: - # - # No arguments: - # {{my_macro}} - # - # With arguments: - # {{my_macro(arg1, arg2)}} - # - # With a block of text: - # {{my_macro - # multiple lines - # of text - # }} - # - # With arguments and a block of text - # {{my_macro(arg1, arg2) - # multiple lines - # of text - # }} - # - # If a block of text is given, the closing tag }} must be at the start of a new line. - def macro(name, options={}, &block) - options.assert_valid_keys(:desc, :parse_args) - unless name.to_s.match(/\A\w+\z/) - raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)" - end - unless block_given? - raise "Can not create a macro without a block!" - end - name = name.to_s.downcase.to_sym - available_macros[name] = {:desc => @@desc || ''}.merge(options) - @@desc = nil - Definitions.send :define_method, "macro_#{name}", &block - end - - # Sets description for the next macro to be defined - def desc(txt) - @@desc = txt - end - end - - # Builtin macros - desc "Sample macro." - macro :hello_world do |obj, args, text| - h("Hello world! Object: #{obj.class.name}, " + - (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") + - " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.") - ) - end - - desc "Displays a list of all available macros, including description if available." - macro :macro_list do |obj, args| - out = ''.html_safe - @@available_macros.each do |macro, options| - out << content_tag('dt', content_tag('code', macro.to_s)) - out << content_tag('dd', textilizable(options[:desc])) - end - content_tag('dl', out) - end - - desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" + - " !{{child_pages}} -- can be used from a wiki page only\n" + - " !{{child_pages(depth=2)}} -- display 2 levels nesting only\n" - " !{{child_pages(Foo)}} -- lists all children of page Foo\n" + - " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo" - macro :child_pages do |obj, args| - args, options = extract_macro_options(args, :parent, :depth) - options[:depth] = options[:depth].to_i if options[:depth].present? - - page = nil - if args.size > 0 - page = Wiki.find_page(args.first.to_s, :project => @project) - elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version) - page = obj.page - else - raise 'With no argument, this macro can be called from wiki pages only.' - end - raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) - pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id) - render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id) - end - - desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}" - macro :include do |obj, args| - page = Wiki.find_page(args.first.to_s, :project => @project) - raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) - @included_wiki_pages ||= [] - raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) - @included_wiki_pages << page.title - out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false) - @included_wiki_pages.pop - out - end - - desc "Inserts of collapsed block of text. Example:\n\n {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}" - macro :collapse do |obj, args, text| - html_id = "collapse-#{Redmine::Utils.random_hex(4)}" - show_label = args[0] || l(:button_show) - hide_label = args[1] || args[0] || l(:button_hide) - js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);" - out = ''.html_safe - out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed') - out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;') - out << content_tag('div', textilizable(text, :object => obj), :id => html_id, :class => 'collapsed-text', :style => 'display:none;') - out - end - - desc "Displays a clickable thumbnail of an attached image. Examples:\n\n
    {{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}
    " - macro :thumbnail do |obj, args| - args, options = extract_macro_options(args, :size, :title) - filename = args.first - raise 'Filename required' unless filename.present? - size = options[:size] - raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/) - size = size.to_i - size = nil unless size > 0 - if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename) - title = options[:title] || attachment.title - img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename) - link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title) - else - raise "Attachment #{filename} not found" - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/32/32f1eb36d32f7e5b14215f433d0d23c189721a5e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/32/32f1eb36d32f7e5b14215f433d0d23c189721a5e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,15 @@ +# Loads the core plugins located in lib/plugins +Dir.glob(File.join(Rails.root, "lib/plugins/*")).sort.each do |directory| + if File.directory?(directory) + lib = File.join(directory, "lib") + if File.directory?(lib) + $:.unshift lib + ActiveSupport::Dependencies.autoload_paths += [lib] + end + initializer = File.join(directory, "init.rb") + if File.file?(initializer) + config = RedmineApp::Application.config + eval(File.read(initializer), binding, initializer) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/33/330c3139ce88daeba1ec5a385dfead443884905c.svn-base --- a/.svn/pristine/33/330c3139ce88daeba1ec5a385dfead443884905c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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__) -require 'mail_handler_controller' - -# Re-raise errors caught by the controller. -class MailHandlerController; def rescue_action(e) raise e end; end - -class MailHandlerControllerTest < ActionController::TestCase - fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses, - :trackers, :projects_trackers, :enumerations - - FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' - - def setup - @controller = MailHandlerController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - end - - def test_should_create_issue - # Enable API and set a key - Setting.mail_handler_api_enabled = 1 - Setting.mail_handler_api_key = 'secret' - - assert_difference 'Issue.count' do - post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) - end - assert_response 201 - end - - def test_should_respond_with_422_if_not_created - Project.find('onlinestore').destroy - - Setting.mail_handler_api_enabled = 1 - Setting.mail_handler_api_key = 'secret' - - assert_no_difference 'Issue.count' do - post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) - end - assert_response 422 - end - - def test_should_not_allow_with_api_disabled - # Disable API - Setting.mail_handler_api_enabled = 0 - Setting.mail_handler_api_key = 'secret' - - assert_no_difference 'Issue.count' do - post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) - end - assert_response 403 - end - - def test_should_not_allow_with_wrong_key - # Disable API - Setting.mail_handler_api_enabled = 1 - Setting.mail_handler_api_key = 'secret' - - assert_no_difference 'Issue.count' do - post :index, :key => 'wrong', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) - end - assert_response 403 - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/33/33196b4d983ac84071a32086264adeca270eeb1b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/33196b4d983ac84071a32086264adeca270eeb1b.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,1114 @@ +# French translations for Ruby on Rails +# by Christian Lescuyer (christian@flyingcoders.com) +# contributor: Sebastien Grosjean - ZenCocoon.com +# contributor: Thibaut Cuvelier - Developpez.com + +fr: + direction: ltr + date: + formats: + default: "%d/%m/%Y" + short: "%e %b" + long: "%e %B %Y" + long_ordinal: "%e %B %Y" + only_day: "%e" + + day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi] + abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam] + month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre] + abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.] + order: + - :day + - :month + - :year + + time: + formats: + default: "%d/%m/%Y %H:%M" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%A %d %B %Y %H:%M:%S %Z" + long_ordinal: "%A %d %B %Y %H:%M:%S %Z" + only_second: "%S" + am: 'am' + pm: 'pm' + + datetime: + distance_in_words: + half_a_minute: "30 secondes" + less_than_x_seconds: + zero: "moins d'une seconde" + one: "moins d'une seconde" + other: "moins de %{count} secondes" + x_seconds: + one: "1 seconde" + other: "%{count} secondes" + less_than_x_minutes: + zero: "moins d'une minute" + one: "moins d'une minute" + other: "moins de %{count} minutes" + x_minutes: + one: "1 minute" + other: "%{count} minutes" + about_x_hours: + one: "environ une heure" + other: "environ %{count} heures" + x_hours: + one: "une heure" + other: "%{count} heures" + x_days: + one: "un jour" + other: "%{count} jours" + about_x_months: + one: "environ un mois" + other: "environ %{count} mois" + x_months: + one: "un mois" + other: "%{count} mois" + about_x_years: + one: "environ un an" + other: "environ %{count} ans" + over_x_years: + one: "plus d'un an" + other: "plus de %{count} ans" + almost_x_years: + one: "presqu'un an" + other: "presque %{count} ans" + prompts: + year: "Année" + month: "Mois" + day: "Jour" + hour: "Heure" + minute: "Minute" + second: "Seconde" + + number: + format: + precision: 3 + separator: ',' + delimiter: ' ' + currency: + format: + unit: '€' + precision: 2 + format: '%n %u' + human: + format: + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "octet" + other: "octet" + kb: "ko" + mb: "Mo" + gb: "Go" + tb: "To" + + support: + array: + sentence_connector: 'et' + skip_last_comma: true + word_connector: ", " + two_words_connector: " et " + last_word_connector: " et " + + activerecord: + errors: + template: + header: + one: "Impossible d'enregistrer %{model} : une erreur" + other: "Impossible d'enregistrer %{model} : %{count} erreurs." + body: "Veuillez vérifier les champs suivants :" + messages: + inclusion: "n'est pas inclus(e) dans la liste" + exclusion: "n'est pas disponible" + invalid: "n'est pas valide" + confirmation: "ne concorde pas avec la confirmation" + accepted: "doit être accepté(e)" + empty: "doit être renseigné(e)" + blank: "doit être renseigné(e)" + too_long: "est trop long (pas plus de %{count} caractères)" + too_short: "est trop court (au moins %{count} caractères)" + wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)" + taken: "est déjà utilisé" + not_a_number: "n'est pas un nombre" + not_a_date: "n'est pas une date valide" + greater_than: "doit être supérieur à %{count}" + greater_than_or_equal_to: "doit être supérieur ou égal à %{count}" + equal_to: "doit être égal à %{count}" + less_than: "doit être inférieur à %{count}" + less_than_or_equal_to: "doit être inférieur ou égal à %{count}" + odd: "doit être impair" + even: "doit être pair" + greater_than_start_date: "doit être postérieure à la date de début" + not_same_project: "n'appartient pas au même projet" + circular_dependency: "Cette relation créerait une dépendance circulaire" + cant_link_an_issue_with_a_descendant: "Une demande ne peut pas être liée à l'une de ses sous-tâches" + earlier_than_minimum_start_date: "ne peut pas être antérieure au %{date} à cause des demandes qui précédent" + + actionview_instancetag_blank_option: Choisir + + general_text_No: 'Non' + general_text_Yes: 'Oui' + general_text_no: 'non' + general_text_yes: 'oui' + general_lang_name: 'Français' + general_csv_separator: ';' + general_csv_decimal_separator: ',' + general_csv_encoding: ISO-8859-1 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Le compte a été mis à jour avec succès. + notice_account_invalid_creditentials: Identifiant ou mot de passe invalide. + notice_account_password_updated: Mot de passe mis à jour avec succès. + notice_account_wrong_password: Mot de passe incorrect + notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé à l'adresse %{email}. + notice_account_unknown_email: Aucun compte ne correspond à cette adresse. + notice_account_not_activated_yet: Vous n'avez pas encore activé votre compte. Si vous voulez recevoir un nouveau message d'activation, veuillez cliquer sur ce lien. + notice_account_locked: Votre compte est verrouillé. + notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe. + notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé. + notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter. + notice_successful_create: Création effectuée avec succès. + notice_successful_update: Mise à jour effectuée avec succès. + notice_successful_delete: Suppression effectuée avec succès. + notice_successful_connection: Connexion réussie. + notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée." + notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible. + notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page." + notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé. + notice_email_sent: "Un email a été envoyé à %{value}" + notice_email_error: "Erreur lors de l'envoi de l'email (%{value})" + notice_feeds_access_key_reseted: "Votre clé d'accès aux flux Atom a été réinitialisée." + notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}." + notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}." + notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour." + notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur." + notice_default_data_loaded: Paramétrage par défaut chargé avec succès. + notice_unable_delete_version: Impossible de supprimer cette version. + notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour. + notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée. + notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})" + notice_issue_successful_create: "Demande %{id} créée." + notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez." + notice_account_deleted: "Votre compte a été définitivement supprimé." + notice_user_successful_create: "Utilisateur %{id} créé." + notice_new_password_must_be_different: Votre nouveau mot de passe doit être différent de votre mot de passe actuel + + error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}" + error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt." + error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}" + error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée." + error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet" + error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte' + error_can_not_archive_project: "Ce projet ne peut pas être archivé" + error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source' + error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles' + error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour. + error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size}) + error_session_expired: "Votre session a expiré. Veuillez vous reconnecter." + + warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés." + + mail_subject_lost_password: "Votre mot de passe %{value}" + mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :' + mail_subject_register: "Activation de votre compte %{value}" + mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :' + mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter." + mail_body_account_information: Paramètres de connexion de votre compte + mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}" + mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :" + mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})" + mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :" + mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée" + mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}." + mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour" + mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}." + + + field_name: Nom + field_description: Description + field_summary: Résumé + field_is_required: Obligatoire + field_firstname: Prénom + field_lastname: Nom + field_mail: "Email " + field_filename: Fichier + field_filesize: Taille + field_downloads: Téléchargements + field_author: Auteur + field_created_on: "Créé " + field_updated_on: "Mis-à-jour " + field_closed_on: Fermé + field_field_format: Format + field_is_for_all: Pour tous les projets + field_possible_values: Valeurs possibles + field_regexp: Expression régulière + field_min_length: Longueur minimum + field_max_length: Longueur maximum + field_value: Valeur + field_category: Catégorie + field_title: Titre + field_project: Projet + field_issue: Demande + field_status: Statut + field_notes: Notes + field_is_closed: Demande fermée + field_is_default: Valeur par défaut + field_tracker: Tracker + field_subject: Sujet + field_due_date: Echéance + field_assigned_to: Assigné à + field_priority: Priorité + field_fixed_version: Version cible + field_user: Utilisateur + field_role: Rôle + field_homepage: "Site web " + field_is_public: Public + field_parent: Sous-projet de + field_is_in_roadmap: Demandes affichées dans la roadmap + field_login: "Identifiant " + field_mail_notification: Notifications par mail + field_admin: Administrateur + field_last_login_on: "Dernière connexion " + field_language: Langue + field_effective_date: Date + field_password: Mot de passe + field_new_password: Nouveau mot de passe + field_password_confirmation: Confirmation + field_version: Version + field_type: Type + field_host: Hôte + field_port: Port + field_account: Compte + field_base_dn: Base DN + field_attr_login: Attribut Identifiant + field_attr_firstname: Attribut Prénom + field_attr_lastname: Attribut Nom + field_attr_mail: Attribut Email + field_onthefly: Création des utilisateurs à la volée + field_start_date: Début + field_done_ratio: "% réalisé" + field_auth_source: Mode d'authentification + field_hide_mail: Cacher mon adresse mail + field_comments: Commentaire + field_url: URL + field_start_page: Page de démarrage + field_subproject: Sous-projet + field_hours: Heures + field_activity: Activité + field_spent_on: Date + field_identifier: Identifiant + field_is_filter: Utilisé comme filtre + field_issue_to: Demande liée + field_delay: Retard + field_assignable: Demandes assignables à ce rôle + field_redirect_existing_links: Rediriger les liens existants + field_estimated_hours: Temps estimé + field_column_names: Colonnes + field_time_zone: Fuseau horaire + field_searchable: Utilisé pour les recherches + field_default_value: Valeur par défaut + field_comments_sorting: Afficher les commentaires + field_parent_title: Page parent + field_editable: Modifiable + field_watcher: Observateur + field_identity_url: URL OpenID + field_content: Contenu + field_group_by: Grouper par + field_sharing: Partage + field_active: Actif + field_parent_issue: Tâche parente + field_visible: Visible + field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé" + field_issues_visibility: Visibilité des demandes + field_is_private: Privée + field_commit_logs_encoding: Encodage des messages de commit + field_repository_is_default: Dépôt principal + field_multiple: Valeurs multiples + field_auth_source_ldap_filter: Filtre LDAP + field_core_fields: Champs standards + field_timeout: "Timeout (en secondes)" + field_board_parent: Forum parent + field_private_notes: Notes privées + field_inherit_members: Hériter les membres + field_generate_password: Générer un mot de passe + field_must_change_passwd: Doit changer de mot de passe à la prochaine connexion + + setting_app_title: Titre de l'application + setting_app_subtitle: Sous-titre de l'application + setting_welcome_text: Texte d'accueil + setting_default_language: Langue par défaut + setting_login_required: Authentification obligatoire + setting_self_registration: Inscription des nouveaux utilisateurs + setting_attachment_max_size: Taille maximale des fichiers + setting_issues_export_limit: Limite d'exportation des demandes + setting_mail_from: Adresse d'émission + setting_bcc_recipients: Destinataires en copie cachée (cci) + setting_plain_text_mail: Mail en texte brut (non HTML) + setting_host_name: Nom d'hôte et chemin + setting_text_formatting: Formatage du texte + setting_wiki_compression: Compression de l'historique des pages wiki + setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom + setting_default_projects_public: Définir les nouveaux projets comme publics par défaut + setting_autofetch_changesets: Récupération automatique des commits + setting_sys_api_enabled: Activer les WS pour la gestion des dépôts + setting_commit_ref_keywords: Mots-clés de référencement + setting_commit_fix_keywords: Mots-clés de résolution + setting_autologin: Durée maximale de connexion automatique + setting_date_format: Format de date + setting_time_format: Format d'heure + setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets + setting_cross_project_subtasks: Autoriser les sous-tâches dans des projets différents + setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes + setting_emails_footer: Pied-de-page des emails + setting_protocol: Protocole + setting_per_page_options: Options d'objets affichés par page + setting_user_format: Format d'affichage des utilisateurs + setting_activity_days_default: Nombre de jours affichés sur l'activité des projets + setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux + setting_enabled_scm: SCM activés + setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes" + setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails" + setting_mail_handler_api_key: Clé de protection de l'API + setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels + setting_gravatar_enabled: Afficher les Gravatar des utilisateurs + setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées + setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne + setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier" + setting_openid: "Autoriser l'authentification et l'enregistrement OpenID" + setting_password_min_length: Longueur minimum des mots de passe + setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet + setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets + setting_issue_done_ratio: Calcul de l'avancement des demandes + setting_issue_done_ratio_issue_status: Utiliser le statut + setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué' + setting_rest_api_enabled: Activer l'API REST + setting_gravatar_default: Image Gravatar par défaut + setting_start_of_week: Jour de début des calendriers + setting_cache_formatted_text: Mettre en cache le texte formaté + setting_commit_logtime_enabled: Permettre la saisie de temps + setting_commit_logtime_activity_id: Activité pour le temps saisi + setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt + setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes + setting_default_issue_start_date_to_creation_date: Donner à la date de début d'une nouvelle demande la valeur de la date du jour + setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets + setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte + setting_session_lifetime: Durée de vie maximale des sessions + setting_session_timeout: Durée maximale d'inactivité + setting_thumbnails_enabled: Afficher les vignettes des images + setting_thumbnails_size: Taille des vignettes (en pixels) + setting_non_working_week_days: Jours non travaillés + setting_jsonp_enabled: Activer le support JSONP + setting_default_projects_tracker_ids: Trackers par défaut pour les nouveaux projets + setting_mail_handler_excluded_filenames: Exclure les fichiers attachés par leur nom + + permission_add_project: Créer un projet + permission_add_subprojects: Créer des sous-projets + permission_edit_project: Modifier le projet + permission_close_project: Fermer / réouvrir le projet + permission_select_project_modules: Choisir les modules + permission_manage_members: Gérer les membres + permission_manage_versions: Gérer les versions + permission_manage_categories: Gérer les catégories de demandes + permission_view_issues: Voir les demandes + permission_add_issues: Créer des demandes + permission_edit_issues: Modifier les demandes + permission_manage_issue_relations: Gérer les relations + permission_set_issues_private: Rendre les demandes publiques ou privées + permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées + permission_add_issue_notes: Ajouter des notes + permission_edit_issue_notes: Modifier les notes + permission_edit_own_issue_notes: Modifier ses propres notes + permission_view_private_notes: Voir les notes privées + permission_set_notes_private: Rendre les notes privées + permission_move_issues: Déplacer les demandes + permission_delete_issues: Supprimer les demandes + permission_manage_public_queries: Gérer les requêtes publiques + permission_save_queries: Sauvegarder les requêtes + permission_view_gantt: Voir le gantt + permission_view_calendar: Voir le calendrier + permission_view_issue_watchers: Voir la liste des observateurs + permission_add_issue_watchers: Ajouter des observateurs + permission_delete_issue_watchers: Supprimer des observateurs + permission_log_time: Saisir le temps passé + permission_view_time_entries: Voir le temps passé + permission_edit_time_entries: Modifier les temps passés + permission_edit_own_time_entries: Modifier son propre temps passé + permission_manage_news: Gérer les annonces + permission_comment_news: Commenter les annonces + permission_view_documents: Voir les documents + permission_add_documents: Ajouter des documents + permission_edit_documents: Modifier les documents + permission_delete_documents: Supprimer les documents + permission_manage_files: Gérer les fichiers + permission_view_files: Voir les fichiers + permission_manage_wiki: Gérer le wiki + permission_rename_wiki_pages: Renommer les pages + permission_delete_wiki_pages: Supprimer les pages + permission_view_wiki_pages: Voir le wiki + permission_view_wiki_edits: "Voir l'historique des modifications" + permission_edit_wiki_pages: Modifier les pages + permission_delete_wiki_pages_attachments: Supprimer les fichiers joints + permission_protect_wiki_pages: Protéger les pages + permission_manage_repository: Gérer le dépôt de sources + permission_browse_repository: Parcourir les sources + permission_view_changesets: Voir les révisions + permission_commit_access: Droit de commit + permission_manage_boards: Gérer les forums + permission_view_messages: Voir les messages + permission_add_messages: Poster un message + permission_edit_messages: Modifier les messages + permission_edit_own_messages: Modifier ses propres messages + permission_delete_messages: Supprimer les messages + permission_delete_own_messages: Supprimer ses propres messages + permission_export_wiki_pages: Exporter les pages + permission_manage_project_activities: Gérer les activités + permission_manage_subtasks: Gérer les sous-tâches + permission_manage_related_issues: Gérer les demandes associées + + project_module_issue_tracking: Suivi des demandes + project_module_time_tracking: Suivi du temps passé + project_module_news: Publication d'annonces + project_module_documents: Publication de documents + project_module_files: Publication de fichiers + project_module_wiki: Wiki + project_module_repository: Dépôt de sources + project_module_boards: Forums de discussion + + label_user: Utilisateur + label_user_plural: Utilisateurs + label_user_new: Nouvel utilisateur + label_user_anonymous: Anonyme + label_project: Projet + label_project_new: Nouveau projet + label_project_plural: Projets + label_x_projects: + zero: aucun projet + one: un projet + other: "%{count} projets" + label_project_all: Tous les projets + label_project_latest: Derniers projets + label_issue: Demande + label_issue_new: Nouvelle demande + label_issue_plural: Demandes + label_issue_view_all: Voir toutes les demandes + label_issue_added: Demande ajoutée + label_issue_updated: Demande mise à jour + label_issue_note_added: Note ajoutée + label_issue_status_updated: Statut changé + label_issue_priority_updated: Priorité changée + label_issues_by: "Demandes par %{value}" + label_document: Document + label_document_new: Nouveau document + label_document_plural: Documents + label_document_added: Document ajouté + label_role: Rôle + label_role_plural: Rôles + label_role_new: Nouveau rôle + label_role_and_permissions: Rôles et permissions + label_role_anonymous: Anonyme + label_role_non_member: Non membre + label_member: Membre + label_member_new: Nouveau membre + label_member_plural: Membres + label_tracker: Tracker + label_tracker_plural: Trackers + label_tracker_new: Nouveau tracker + label_workflow: Workflow + label_issue_status: Statut de demandes + label_issue_status_plural: Statuts de demandes + label_issue_status_new: Nouveau statut + label_issue_category: Catégorie de demandes + label_issue_category_plural: Catégories de demandes + label_issue_category_new: Nouvelle catégorie + label_custom_field: Champ personnalisé + label_custom_field_plural: Champs personnalisés + label_custom_field_new: Nouveau champ personnalisé + label_enumerations: Listes de valeurs + label_enumeration_new: Nouvelle valeur + label_information: Information + label_information_plural: Informations + label_please_login: Identification + label_register: S'enregistrer + label_login_with_open_id_option: S'authentifier avec OpenID + label_password_lost: Mot de passe perdu + label_home: Accueil + label_my_page: Ma page + label_my_account: Mon compte + label_my_projects: Mes projets + label_my_page_block: Blocs disponibles + label_administration: Administration + label_login: Connexion + label_logout: Déconnexion + label_help: Aide + label_reported_issues: "Demandes soumises " + label_assigned_to_me_issues: Demandes qui me sont assignées + label_last_login: "Dernière connexion " + label_registered_on: "Inscrit le " + label_activity: Activité + label_overall_activity: Activité globale + label_user_activity: "Activité de %{value}" + label_new: Nouveau + label_logged_as: Connecté en tant que + label_environment: Environnement + label_authentication: Authentification + label_auth_source: Mode d'authentification + label_auth_source_new: Nouveau mode d'authentification + label_auth_source_plural: Modes d'authentification + label_subproject_plural: Sous-projets + label_subproject_new: Nouveau sous-projet + label_and_its_subprojects: "%{value} et ses sous-projets" + label_min_max_length: Longueurs mini - maxi + label_list: Liste + label_date: Date + label_integer: Entier + label_float: Nombre décimal + label_boolean: Booléen + label_string: Texte + label_text: Texte long + label_attribute: Attribut + label_attribute_plural: Attributs + label_no_data: Aucune donnée à afficher + label_change_status: Changer le statut + label_history: Historique + label_attachment: Fichier + label_attachment_new: Nouveau fichier + label_attachment_delete: Supprimer le fichier + label_attachment_plural: Fichiers + label_file_added: Fichier ajouté + label_report: Rapport + label_report_plural: Rapports + label_news: Annonce + label_news_new: Nouvelle annonce + label_news_plural: Annonces + label_news_latest: Dernières annonces + label_news_view_all: Voir toutes les annonces + label_news_added: Annonce ajoutée + label_news_comment_added: Commentaire ajouté à une annonce + label_settings: Configuration + label_overview: Aperçu + label_version: Version + label_version_new: Nouvelle version + label_version_plural: Versions + label_confirmation: Confirmation + label_export_to: 'Formats disponibles :' + label_read: Lire... + label_public_projects: Projets publics + label_open_issues: ouvert + label_open_issues_plural: ouverts + label_closed_issues: fermé + label_closed_issues_plural: fermés + label_x_open_issues_abbr_on_total: + zero: 0 ouverte sur %{total} + one: 1 ouverte sur %{total} + other: "%{count} ouvertes sur %{total}" + label_x_open_issues_abbr: + zero: 0 ouverte + one: 1 ouverte + other: "%{count} ouvertes" + label_x_closed_issues_abbr: + zero: 0 fermée + one: 1 fermée + other: "%{count} fermées" + label_x_issues: + zero: 0 demande + one: 1 demande + other: "%{count} demandes" + label_total: Total + label_total_time: Temps total + label_permissions: Permissions + label_current_status: Statut actuel + label_new_statuses_allowed: Nouveaux statuts autorisés + label_all: tous + label_any: tous + label_none: aucun + label_nobody: personne + label_next: Suivant + label_previous: Précédent + label_used_by: Utilisé par + label_details: Détails + label_add_note: Ajouter une note + label_per_page: Par page + label_calendar: Calendrier + label_months_from: mois depuis + label_gantt: Gantt + label_internal: Interne + label_last_changes: "%{count} derniers changements" + label_change_view_all: Voir tous les changements + label_personalize_page: Personnaliser cette page + label_comment: Commentaire + label_comment_plural: Commentaires + label_x_comments: + zero: aucun commentaire + one: un commentaire + other: "%{count} commentaires" + label_comment_add: Ajouter un commentaire + label_comment_added: Commentaire ajouté + label_comment_delete: Supprimer les commentaires + label_query: Rapport personnalisé + label_query_plural: Rapports personnalisés + label_query_new: Nouveau rapport + label_my_queries: Mes rapports personnalisés + label_filter_add: "Ajouter le filtre " + label_filter_plural: Filtres + label_equals: égal + label_not_equals: différent + label_in_less_than: dans moins de + label_in_more_than: dans plus de + label_in_the_next_days: dans les prochains jours + label_in_the_past_days: dans les derniers jours + label_in: dans + label_today: aujourd'hui + label_all_time: toute la période + label_yesterday: hier + label_this_week: cette semaine + label_last_week: la semaine dernière + label_last_n_weeks: "les %{count} dernières semaines" + label_last_n_days: "les %{count} derniers jours" + label_this_month: ce mois-ci + label_last_month: le mois dernier + label_this_year: cette année + label_date_range: Période + label_less_than_ago: il y a moins de + label_more_than_ago: il y a plus de + label_ago: il y a + label_contains: contient + label_not_contains: ne contient pas + label_any_issues_in_project: une demande du projet + label_any_issues_not_in_project: une demande hors du projet + label_no_issues_in_project: aucune demande du projet + label_day_plural: jours + label_repository: Dépôt + label_repository_new: Nouveau dépôt + label_repository_plural: Dépôts + label_browse: Parcourir + label_revision: "Révision " + label_revision_plural: Révisions + label_associated_revisions: Révisions associées + label_added: ajouté + label_modified: modifié + label_copied: copié + label_renamed: renommé + label_deleted: supprimé + label_latest_revision: Dernière révision + label_latest_revision_plural: Dernières révisions + label_view_revisions: Voir les révisions + label_max_size: Taille maximale + label_sort_highest: Remonter en premier + label_sort_higher: Remonter + label_sort_lower: Descendre + label_sort_lowest: Descendre en dernier + label_roadmap: Roadmap + label_roadmap_due_in: "Échéance dans %{value}" + label_roadmap_overdue: "En retard de %{value}" + label_roadmap_no_issues: Aucune demande pour cette version + label_search: "Recherche " + label_result_plural: Résultats + label_all_words: Tous les mots + label_wiki: Wiki + label_wiki_edit: Révision wiki + label_wiki_edit_plural: Révisions wiki + label_wiki_page: Page wiki + label_wiki_page_plural: Pages wiki + label_index_by_title: Index par titre + label_index_by_date: Index par date + label_current_version: Version actuelle + label_preview: Prévisualisation + label_feed_plural: Flux Atom + label_changes_details: Détails de tous les changements + label_issue_tracking: Suivi des demandes + label_spent_time: Temps passé + label_f_hour: "%{value} heure" + label_f_hour_plural: "%{value} heures" + label_time_tracking: Suivi du temps + label_change_plural: Changements + label_statistics: Statistiques + label_commits_per_month: Commits par mois + label_commits_per_author: Commits par auteur + label_view_diff: Voir les différences + label_diff_inline: en ligne + label_diff_side_by_side: côte à côte + label_options: Options + label_copy_workflow_from: Copier le workflow de + label_permissions_report: Synthèse des permissions + label_watched_issues: Demandes surveillées + label_related_issues: Demandes liées + label_applied_status: Statut appliqué + label_loading: Chargement... + label_relation_new: Nouvelle relation + label_relation_delete: Supprimer la relation + label_relates_to: Lié à + label_duplicates: Duplique + label_duplicated_by: Dupliqué par + label_blocks: Bloque + label_blocked_by: Bloqué par + label_precedes: Précède + label_follows: Suit + label_copied_to: Copié vers + label_copied_from: Copié depuis + label_end_to_start: fin à début + label_end_to_end: fin à fin + label_start_to_start: début à début + label_start_to_end: début à fin + label_stay_logged_in: Rester connecté + label_disabled: désactivé + label_show_completed_versions: Voir les versions passées + label_me: moi + label_board: Forum + label_board_new: Nouveau forum + label_board_plural: Forums + label_topic_plural: Discussions + label_message_plural: Messages + label_message_last: Dernier message + label_message_new: Nouveau message + label_message_posted: Message ajouté + label_reply_plural: Réponses + label_send_information: Envoyer les informations à l'utilisateur + label_year: Année + label_month: Mois + label_week: Semaine + label_date_from: Du + label_date_to: Au + label_language_based: Basé sur la langue de l'utilisateur + label_sort_by: "Trier par %{value}" + label_send_test_email: Envoyer un email de test + label_feeds_access_key_created_on: "Clé d'accès Atom créée il y a %{value}" + label_module_plural: Modules + label_added_time_by: "Ajouté par %{author} il y a %{age}" + label_updated_time_by: "Mis à jour par %{author} il y a %{age}" + label_updated_time: "Mis à jour il y a %{value}" + label_jump_to_a_project: Aller à un projet... + label_file_plural: Fichiers + label_changeset_plural: Révisions + label_default_columns: Colonnes par défaut + label_no_change_option: (Pas de changement) + label_bulk_edit_selected_issues: Modifier les demandes sélectionnées + label_theme: Thème + label_default: Défaut + label_search_titles_only: Uniquement dans les titres + label_user_mail_option_all: "Pour tous les événements de tous mes projets" + label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..." + label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue" + label_registration_activation_by_email: activation du compte par email + label_registration_manual_activation: activation manuelle du compte + label_registration_automatic_activation: activation automatique du compte + label_display_per_page: "Par page : %{value}" + label_age: Âge + label_change_properties: Changer les propriétés + label_general: Général + label_more: Plus + label_scm: SCM + label_plugins: Plugins + label_ldap_authentication: Authentification LDAP + label_downloads_abbr: D/L + label_optional_description: Description facultative + label_add_another_file: Ajouter un autre fichier + label_preferences: Préférences + label_chronological_order: Dans l'ordre chronologique + label_reverse_chronological_order: Dans l'ordre chronologique inverse + label_planning: Planning + label_incoming_emails: Emails entrants + label_generate_key: Générer une clé + label_issue_watchers: Observateurs + label_example: Exemple + label_display: Affichage + label_sort: Tri + label_ascending: Croissant + label_descending: Décroissant + label_date_from_to: Du %{start} au %{end} + label_wiki_content_added: Page wiki ajoutée + label_wiki_content_updated: Page wiki mise à jour + label_group_plural: Groupes + label_group: Groupe + label_group_new: Nouveau groupe + label_time_entry_plural: Temps passé + label_version_sharing_none: Non partagé + label_version_sharing_descendants: Avec les sous-projets + label_version_sharing_hierarchy: Avec toute la hiérarchie + label_version_sharing_tree: Avec tout l'arbre + label_version_sharing_system: Avec tous les projets + label_copy_source: Source + label_copy_target: Cible + label_copy_same_as_target: Comme la cible + label_update_issue_done_ratios: Mettre à jour l'avancement des demandes + label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker + label_api_access_key: Clé d'accès API + label_api_access_key_created_on: Clé d'accès API créée il y a %{value} + label_feeds_access_key: Clé d'accès Atom + label_missing_api_access_key: Clé d'accès API manquante + label_missing_feeds_access_key: Clé d'accès Atom manquante + label_close_versions: Fermer les versions terminées + label_revision_id: Révision %{value} + label_profile: Profil + label_subtask_plural: Sous-tâches + label_project_copy_notifications: Envoyer les notifications durant la copie du projet + label_principal_search: "Rechercher un utilisateur ou un groupe :" + label_user_search: "Rechercher un utilisateur :" + label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande + label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur + label_issues_visibility_all: Toutes les demandes + label_issues_visibility_public: Toutes les demandes non privées + label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur + label_export_options: Options d'exportation %{export_format} + label_copy_attachments: Copier les fichiers + label_copy_subtasks: Copier les sous-tâches + label_item_position: "%{position} sur %{count}" + label_completed_versions: Versions passées + label_session_expiration: Expiration des sessions + label_show_closed_projects: Voir les projets fermés + label_status_transitions: Changements de statut + label_fields_permissions: Permissions sur les champs + label_readonly: Lecture + label_required: Obligatoire + label_hidden: Caché + label_attribute_of_project: "%{name} du projet" + label_attribute_of_issue: "%{name} de la demande" + label_attribute_of_author: "%{name} de l'auteur" + label_attribute_of_assigned_to: "%{name} de l'assigné" + label_attribute_of_user: "%{name} de l'utilisateur" + label_attribute_of_fixed_version: "%{name} de la version cible" + label_cross_project_descendants: Avec les sous-projets + label_cross_project_tree: Avec tout l'arbre + label_cross_project_hierarchy: Avec toute la hiérarchie + label_cross_project_system: Avec tous les projets + label_gantt_progress_line: Ligne de progression + label_visibility_private: par moi uniquement + label_visibility_roles: par ces roles uniquement + label_visibility_public: par tout le monde + + button_login: Connexion + button_submit: Soumettre + button_save: Sauvegarder + button_check_all: Tout cocher + button_uncheck_all: Tout décocher + button_collapse_all: Plier tout + button_expand_all: Déplier tout + button_delete: Supprimer + button_create: Créer + button_create_and_continue: Créer et continuer + button_test: Tester + button_edit: Modifier + button_add: Ajouter + button_change: Changer + button_apply: Appliquer + button_clear: Effacer + button_lock: Verrouiller + button_unlock: Déverrouiller + button_download: Télécharger + button_list: Lister + button_view: Voir + button_move: Déplacer + button_move_and_follow: Déplacer et suivre + button_back: Retour + button_cancel: Annuler + button_activate: Activer + button_sort: Trier + button_log_time: Saisir temps + button_rollback: Revenir à cette version + button_watch: Surveiller + button_unwatch: Ne plus surveiller + button_reply: Répondre + button_archive: Archiver + button_unarchive: Désarchiver + button_reset: Réinitialiser + button_rename: Renommer + button_change_password: Changer de mot de passe + button_copy: Copier + button_copy_and_follow: Copier et suivre + button_annotate: Annoter + button_update: Mettre à jour + button_configure: Configurer + button_quote: Citer + button_duplicate: Dupliquer + button_show: Afficher + button_hide: Cacher + button_edit_section: Modifier cette section + button_export: Exporter + button_delete_my_account: Supprimer mon compte + button_close: Fermer + button_reopen: Réouvrir + + status_active: actif + status_registered: enregistré + status_locked: verrouillé + + project_status_active: actif + project_status_closed: fermé + project_status_archived: archivé + + version_status_open: ouvert + version_status_locked: verrouillé + version_status_closed: fermé + + text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 pour aucune restriction + text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ? + text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés." + text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow + text_are_you_sure: Êtes-vous sûr ? + text_tip_issue_begin_day: tâche commençant ce jour + text_tip_issue_end_day: tâche finissant ce jour + text_tip_issue_begin_end_day: tâche commençant et finissant ce jour + text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés, doit commencer par une minuscule.
    Un fois sauvegardé, l''identifiant ne pourra plus être modifié.' + text_caracters_maximum: "%{count} caractères maximum." + text_caracters_minimum: "%{count} caractères minimum." + text_length_between: "Longueur comprise entre %{min} et %{max} caractères." + text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker + text_unallowed_characters: Caractères non autorisés + text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules). + text_line_separated: Plusieurs valeurs possibles (une valeur par ligne). + text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits + text_issue_added: "La demande %{id} a été soumise par %{author}." + text_issue_updated: "La demande %{id} a été mise à jour par %{author}." + text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ? + text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?" + text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie + text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie + text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)." + text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé." + text_load_default_configuration: Charger le paramétrage par défaut + text_status_changed_by_changeset: "Appliqué par commit %{value}." + text_time_logged_by_changeset: "Appliqué par commit %{value}" + text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?' + text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)." + text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :' + text_default_administrator_account_changed: Compte administrateur par défaut changé + text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture + text_plugin_assets_writable: Répertoire public des plugins accessible en écriture + text_rmagick_available: Bibliothèque RMagick présente (optionnelle) + text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?" + text_destroy_time_entries: Supprimer les heures + text_assign_time_entries_to_project: Reporter les heures sur le projet + text_reassign_time_entries: 'Reporter les heures sur cette demande:' + text_user_wrote: "%{value} a écrit :" + text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets." + text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:' + text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/configuration.yml et redémarrez l'application pour les activer." + text_repository_usernames_mapping: "Vous pouvez sélectionner ou modifier l'utilisateur Redmine associé à chaque nom d'utilisateur figurant dans l'historique du dépôt.\nLes utilisateurs avec le même identifiant ou la même adresse mail seront automatiquement associés." + text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.' + text_custom_field_possible_values_info: 'Une ligne par valeur' + text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?" + text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines" + text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes" + text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page" + text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?" + text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page." + text_issue_conflict_resolution_overwrite: "Appliquer quand même ma mise à jour (les notes précédentes seront conservées mais des changements pourront être écrasés)" + text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements" + text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}" + text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver." + text_session_expiration_settings: "Attention : le changement de ces paramètres peut entrainer l'expiration des sessions utilisateurs en cours, y compris la vôtre." + text_project_closed: Ce projet est fermé et accessible en lecture seule. + text_turning_multiple_off: "Si vous désactivez les valeurs multiples, les valeurs multiples seront supprimées pour n'en conserver qu'une par objet." + + default_role_manager: "Manager " + default_role_developer: "Développeur " + default_role_reporter: "Rapporteur " + default_tracker_bug: Anomalie + default_tracker_feature: Evolution + default_tracker_support: Assistance + default_issue_status_new: Nouveau + default_issue_status_in_progress: En cours + default_issue_status_resolved: Résolu + default_issue_status_feedback: Commentaire + default_issue_status_closed: Fermé + default_issue_status_rejected: Rejeté + default_doc_category_user: Documentation utilisateur + default_doc_category_tech: Documentation technique + default_priority_low: Bas + default_priority_normal: Normal + default_priority_high: Haut + default_priority_urgent: Urgent + default_priority_immediate: Immédiat + default_activity_design: Conception + default_activity_development: Développement + + enumeration_issue_priorities: Priorités des demandes + enumeration_doc_categories: Catégories des documents + enumeration_activities: Activités (suivi du temps) + label_greater_or_equal: ">=" + label_less_or_equal: "<=" + label_between: entre + label_view_all_revisions: Voir toutes les révisions + label_tag: Tag + label_branch: Branche + error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet." + error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)." + text_journal_changed: "%{label} changé de %{old} à %{new}" + text_journal_changed_no_detail: "%{label} mis à jour" + text_journal_set_to: "%{label} mis à %{value}" + text_journal_deleted: "%{label} %{old} supprimé" + text_journal_added: "%{label} %{value} ajouté" + enumeration_system_activity: Activité système + label_board_sticky: Sticky + label_board_locked: Verrouillé + error_unable_delete_issue_status: Impossible de supprimer le statut de demande + error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé + error_unable_to_connect: Connexion impossible (%{value}) + error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé. + error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé. + field_principal: Principal + notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}." + text_zoom_out: Zoom arrière + text_zoom_in: Zoom avant + notice_unable_delete_time_entry: Impossible de supprimer le temps passé. + label_overall_spent_time: Temps passé global + field_time_entries: Temps passé + project_module_gantt: Gantt + project_module_calendar: Calendrier + button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}" + field_text: Champ texte + label_user_mail_option_only_owner: Seulement pour ce que j'ai créé + setting_default_notification_option: Option de notification par défaut + label_user_mail_option_only_my_events: Seulement pour ce que je surveille + label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné + label_user_mail_option_none: Aucune notification + field_member_of_group: Groupe de l'assigné + field_assigned_to_role: Rôle de l'assigné + setting_emails_header: En-tête des emails + label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés + text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?" + field_scm_path_encoding: Encodage des chemins + text_scm_path_encoding_note: "Défaut : UTF-8" + field_path_to_repository: Chemin du dépôt + field_root_directory: Répertoire racine + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)" + text_scm_command: Commande + text_scm_command_version: Version + label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires + text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification. + text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration. + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Ordre de tri + description_project_scope: Périmètre de recherche + description_filter: Filtre + description_user_mail_notification: Option de notification + description_date_from: Date de début + description_message_content: Contenu du message + description_available_columns: Colonnes disponibles + description_all_columns: Toutes les colonnes + description_date_range_interval: Choisir une période + description_issue_category_reassign: Choisir une catégorie + description_search: Champ de recherche + description_notes: Notes + description_date_range_list: Choisir une période prédéfinie + description_choose_project: Projets + description_date_to: Date de fin + description_query_sort_criteria_attribute: Critère de tri + description_wiki_subpages_reassign: Choisir une nouvelle page parent + description_selected_columns: Colonnes sélectionnées + label_parent_revision: Parent + label_child_revision: Enfant + error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale. + setting_repositories_encodings: Encodages des fichiers et des dépôts + label_search_for_watchers: Rechercher des observateurs + text_repository_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres, tirets et tirets bas sont autorisés.
    Un fois sauvegardé, l''identifiant ne pourra plus être modifié.' + text_convert_available: Binaire convert de ImageMagick présent (optionel) diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/33/33275847ce5564fa4ff57de7cb764a0f4a778730.svn-base --- a/.svn/pristine/33/33275847ce5564fa4ff57de7cb764a0f4a778730.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -

    <%=l(:label_report_plural)%>

    - -
    -

    <%=l(:field_tracker)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'tracker' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %> -
    -

    <%=l(:field_priority)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'priority' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_priority, :field_name => "priority_id", :rows => @priorities } %> -
    -

    <%=l(:field_assigned_to)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'assigned_to' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_assigned_to, :field_name => "assigned_to_id", :rows => @assignees } %> -
    -

    <%=l(:field_author)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'author' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_author, :field_name => "author_id", :rows => @authors } %> -
    -<%= call_hook(:view_reports_issue_report_split_content_left, :project => @project) %> -
    - -
    -

    <%=l(:field_version)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'version' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %> -
    -<% if @project.children.any? %> -

    <%=l(:field_subproject)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'subproject' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_subproject, :field_name => "project_id", :rows => @subprojects } %> -
    -<% end %> -

    <%=l(:field_category)%>  <%= link_to image_tag('zoom_in.png'), :action => 'issue_report_details', :detail => 'category' %>

    -<%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %> -
    -<%= call_hook(:view_reports_issue_report_split_content_right, :project => @project) %> -
    - diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/33/334c69118b9eeb233bc531edf269bf4abad61e24.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/334c69118b9eeb233bc531edf269bf4abad61e24.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RoutingAccountTest < ActionController::IntegrationTest + def test_account + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/login" }, + { :controller => 'account', :action => 'login' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/logout" }, + { :controller => 'account', :action => 'logout' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/account/register" }, + { :controller => 'account', :action => 'register' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/account/lost_password" }, + { :controller => 'account', :action => 'lost_password' } + ) + end + assert_routing( + { :method => 'get', :path => "/account/activate" }, + { :controller => 'account', :action => 'activate' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/33/3370cd22f75e851f0796f6234d205c9ce519841c.svn-base --- a/.svn/pristine/33/3370cd22f75e851f0796f6234d205c9ce519841c.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ProjectsHelperTest < ActionView::TestCase - include ApplicationHelper - include ProjectsHelper - include ERB::Util - - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :versions, - :projects_trackers, - :member_roles, - :members, - :groups_users, - :enabled_modules, - :workflows - - def setup - super - set_language_if_valid('en') - User.current = nil - end - - def test_link_to_version_within_project - @project = Project.find(2) - User.current = User.find(1) - assert_equal 'Alpha', link_to_version(Version.find(5)) - end - - def test_link_to_version - User.current = User.find(1) - assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5)) - end - - def test_link_to_private_version - assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5)) - end - - def test_link_to_version_invalid_version - assert_equal '', link_to_version(Object) - end - - def test_format_version_name_within_project - @project = Project.find(1) - assert_equal "0.1", format_version_name(Version.find(1)) - end - - def test_format_version_name - assert_equal "eCookbook - 0.1", format_version_name(Version.find(1)) - end - - def test_format_version_name_for_system_version - assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7)) - end - - def test_version_options_for_select_with_no_versions - assert_equal '', version_options_for_select([]) - assert_equal '', version_options_for_select([], Version.find(1)) - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/33/33f1e1dee1ea9d39975afb9ebca4632d0c5e2445.svn-base --- a/.svn/pristine/33/33f1e1dee1ea9d39975afb9ebca4632d0c5e2445.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 UserPreferenceTest < ActiveSupport::TestCase - fixtures :users, :user_preferences - - def test_create - user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") - user.login = "newuser" - user.password, user.password_confirmation = "password", "password" - assert user.save - - assert_kind_of UserPreference, user.pref - assert_kind_of Hash, user.pref.others - assert user.pref.save - end - - def test_update - user = User.find(1) - assert_equal true, user.pref.hide_mail - user.pref['preftest'] = 'value' - assert user.pref.save - - user.reload - assert_equal 'value', user.pref['preftest'] - end - - def test_others_hash - user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") - user.login = "newuser" - user.password, user.password_confirmation = "password", "password" - assert user.save - assert_nil user.preference - up = UserPreference.new(:user => user) - assert_kind_of Hash, up.others - up.others = nil - assert_nil up.others - assert up.save - assert_kind_of Hash, up.others - end - - def test_reading_value_from_nil_others_hash - up = UserPreference.new(:user => User.new) - up.others = nil - assert_nil up.others - assert_nil up[:foo] - end - - def test_writing_value_to_nil_others_hash - up = UserPreference.new(:user => User.new) - up.others = nil - assert_nil up.others - up[:foo] = 'bar' - assert_equal 'bar', up[:foo] - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/34/342285b027af8a7f1e30f8ee50c822bf5f5c7ecb.svn-base --- a/.svn/pristine/34/342285b027af8a7f1e30f8ee50c822bf5f5c7ecb.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 d98d22a98252 -r fb9a13467253 .svn/pristine/34/345b699f46121e103fdce4587bf3ec87db1cc445.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/345b699f46121e103fdce4587bf3ec87db1cc445.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,107 @@ +module ActiveRecord + module Acts + module Tree + def self.included(base) + base.extend(ClassMethods) + end + + # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children + # association. This requires that you have a foreign key column, which by default is called +parent_id+. + # + # class Category < ActiveRecord::Base + # acts_as_tree :order => "name" + # end + # + # Example: + # root + # \_ child1 + # \_ subchild1 + # \_ subchild2 + # + # root = Category.create("name" => "root") + # child1 = root.children.create("name" => "child1") + # subchild1 = child1.children.create("name" => "subchild1") + # + # root.parent # => nil + # child1.parent # => root + # root.children # => [child1] + # root.children.first.children.first # => subchild1 + # + # In addition to the parent and children associations, the following instance methods are added to the class + # after calling acts_as_tree: + # * siblings - Returns all the children of the parent, excluding the current node ([subchild2] when called on subchild1) + # * self_and_siblings - Returns all the children of the parent, including the current node ([subchild1, subchild2] when called on subchild1) + # * ancestors - Returns all the ancestors of the current node ([child1, root] when called on subchild2) + # * root - Returns the root of the current node (root when called on subchild2) + module ClassMethods + # Configuration options are: + # + # * foreign_key - specifies the column name to use for tracking of the tree (default: +parent_id+) + # * order - makes it possible to sort the children according to this SQL snippet. + # * counter_cache - keeps a count in a +children_count+ column if set to +true+ (default: +false+). + def acts_as_tree(options = {}) + configuration = { :foreign_key => "parent_id", :dependent => :destroy, :order => nil, :counter_cache => nil } + configuration.update(options) if options.is_a?(Hash) + + belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache] + has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent] + + scope :roots, where("#{configuration[:foreign_key]} IS NULL").order(configuration[:order]) + + send :include, ActiveRecord::Acts::Tree::InstanceMethods + end + end + + module InstanceMethods + # Returns list of ancestors, starting from parent until root. + # + # subchild1.ancestors # => [child1, root] + def ancestors + node, nodes = self, [] + nodes << node = node.parent while node.parent + nodes + end + + # Returns list of descendants. + # + # root.descendants # => [child1, subchild1, subchild2] + def descendants(depth=nil) + depth ||= 0 + result = children.dup + unless depth == 1 + result += children.collect {|child| child.descendants(depth-1)}.flatten + end + result + end + + # Returns list of descendants and a reference to the current node. + # + # root.self_and_descendants # => [root, child1, subchild1, subchild2] + def self_and_descendants(depth=nil) + [self] + descendants(depth) + end + + # Returns the root node of the tree. + def root + node = self + node = node.parent while node.parent + node + end + + # Returns all siblings of the current node. + # + # subchild1.siblings # => [subchild2] + def siblings + self_and_siblings - [self] + end + + # Returns all siblings and a reference to the current node. + # + # subchild1.self_and_siblings # => [subchild1, subchild2] + def self_and_siblings + parent ? parent.children : self.class.roots + end + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/34/345f7acae2c1ee68bbdbe5e119f5efcf6fb9fb67.svn-base --- a/.svn/pristine/34/345f7acae2c1ee68bbdbe5e119f5efcf6fb9fb67.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 d98d22a98252 -r fb9a13467253 .svn/pristine/34/345fb72e40574b54a9481a84a098650cf151872f.svn-base --- a/.svn/pristine/34/345fb72e40574b54a9481a84a098650cf151872f.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 QueriesController < ApplicationController - menu_item :issues - before_filter :find_query, :except => [:new, :create, :index] - before_filter :find_optional_project, :only => [:new, :create] - - accept_api_auth :index - - include QueriesHelper - - def index - case params[:format] - when 'xml', 'json' - @offset, @limit = api_offset_and_limit - else - @limit = per_page_option - end - - @query_count = Query.visible.count - @query_pages = Paginator.new self, @query_count, @limit, params['page'] - @queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name") - - respond_to do |format| - format.html { render :nothing => true } - format.api - end - end - - def new - @query = Query.new - @query.user = User.current - @query.project = @project - @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? - build_query_from_params - end - - def create - @query = Query.new(params[:query]) - @query.user = User.current - @query.project = params[:query_is_for_all] ? nil : @project - @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? - build_query_from_params - @query.column_names = nil if params[:default_columns] - - if @query.save - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query - else - render :action => 'new', :layout => !request.xhr? - end - end - - def edit - end - - def update - @query.attributes = params[:query] - @query.project = nil if params[:query_is_for_all] - @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? - build_query_from_params - @query.column_names = nil if params[:default_columns] - - if @query.save - flash[:notice] = l(:notice_successful_update) - redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query - else - render :action => 'edit' - end - end - - def destroy - @query.destroy - redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 - end - -private - def find_query - @query = Query.find(params[:id]) - @project = @query.project - render_403 unless @query.editable_by?(User.current) - rescue ActiveRecord::RecordNotFound - render_404 - end - - def find_optional_project - @project = Project.find(params[:project_id]) if params[:project_id] - render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true) - rescue ActiveRecord::RecordNotFound - render_404 - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/34/34944e69635f53bef7eb7777ca3abc691bf31c39.svn-base --- a/.svn/pristine/34/34944e69635f53bef7eb7777ca3abc691bf31c39.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -var revisionGraph = null; - -function drawRevisionGraph(holder, commits_hash, graph_space) { - var XSTEP = 20, - CIRCLE_INROW_OFFSET = 10; - var commits_by_scmid = commits_hash, - commits = $.map(commits_by_scmid, function(val,i){return val;}); - var max_rdmid = commits.length - 1; - var commit_table_rows = $('table.changesets tr.changeset'); - - // create graph - if(revisionGraph != null) - revisionGraph.clear(); - else - revisionGraph = Raphael(holder); - - var top = revisionGraph.set(); - // init dimensions - var graph_x_offset = commit_table_rows.first().find('td').first().position().left - $(holder).position().left, - graph_y_offset = $(holder).position().top, - graph_right_side = graph_x_offset + (graph_space + 1) * XSTEP, - graph_bottom = commit_table_rows.last().position().top + commit_table_rows.last().height() - graph_y_offset; - - revisionGraph.setSize(graph_right_side, graph_bottom); - - // init colors - var colors = []; - Raphael.getColor.reset(); - for (var k = 0; k <= graph_space; k++) { - colors.push(Raphael.getColor()); - } - - var parent_commit; - var x, y, parent_x, parent_y; - var path, title; - var revision_dot_overlay; - $.each(commits, function(index, commit) { - if (!commit.hasOwnProperty("space")) - commit.space = 0; - - y = commit_table_rows.eq(max_rdmid - commit.rdmid).position().top - graph_y_offset + CIRCLE_INROW_OFFSET; - x = graph_x_offset + XSTEP / 2 + XSTEP * commit.space; - revisionGraph.circle(x, y, 3) - .attr({ - fill: colors[commit.space], - stroke: 'none', - }).toFront(); - // paths to parents - $.each(commit.parent_scmids, function(index, parent_scmid) { - parent_commit = commits_by_scmid[parent_scmid]; - if (parent_commit) { - if (!parent_commit.hasOwnProperty("space")) - parent_commit.space = 0; - - parent_y = commit_table_rows.eq(max_rdmid - parent_commit.rdmid).position().top - graph_y_offset + CIRCLE_INROW_OFFSET; - parent_x = graph_x_offset + XSTEP / 2 + XSTEP * parent_commit.space; - if (parent_commit.space == commit.space) { - // vertical path - path = revisionGraph.path([ - 'M', x, y, - 'V', parent_y]); - } else { - // path to a commit in a different branch (Bezier curve) - path = revisionGraph.path([ - 'M', x, y, - 'C', x, y, x, y + (parent_y - y) / 2, x + (parent_x - x) / 2, y + (parent_y - y) / 2, - 'C', x + (parent_x - x) / 2, y + (parent_y - y) / 2, parent_x, parent_y-(parent_y-y)/2, parent_x, parent_y]); - } - } else { - // vertical path ending at the bottom of the revisionGraph - path = revisionGraph.path([ - 'M', x, y, - 'V', graph_bottom]); - } - path.attr({stroke: colors[commit.space], "stroke-width": 1.5}).toBack(); - }); - revision_dot_overlay = revisionGraph.circle(x, y, 10); - revision_dot_overlay - .attr({ - fill: '#000', - opacity: 0, - cursor: 'pointer', - href: commit.href - }); - - if(commit.refs != null && commit.refs.length > 0) { - title = document.createElementNS(revisionGraph.canvas.namespaceURI, 'title'); - title.appendChild(document.createTextNode(commit.refs)); - revision_dot_overlay.node.appendChild(title); - } - top.push(revision_dot_overlay); - }); - top.toFront(); -}; diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/34/349bb4552fd5e3ee5085280e36098a4598dd3ace.svn-base --- a/.svn/pristine/34/349bb4552fd5e3ee5085280e36098a4598dd3ace.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,130 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 AuthSourceLdapTest < ActiveSupport::TestCase - include Redmine::I18n - fixtures :auth_sources - - def setup - end - - def test_create - a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName') - assert a.save - end - - def test_should_strip_ldap_attributes - a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', - :attr_firstname => 'givenName ') - assert a.save - assert_equal 'givenName', a.reload.attr_firstname - end - - def test_replace_port_zero_to_389 - a = AuthSourceLdap.new( - :name => 'My LDAP', :host => 'ldap.example.net', :port => 0, - :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', - :attr_firstname => 'givenName ') - assert a.save - assert_equal 389, a.port - end - - def test_filter_should_be_validated - set_language_if_valid 'en' - - a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn') - a.filter = "(mail=*@redmine.org" - assert !a.valid? - assert_include "LDAP filter is invalid", a.errors.full_messages - - a.filter = "(mail=*@redmine.org)" - assert a.valid? - end - - if ldap_configured? - context '#authenticate' do - setup do - @auth = AuthSourceLdap.find(1) - @auth.update_attribute :onthefly_register, true - end - - context 'with a valid LDAP user' do - should 'return the user attributes' do - attributes = @auth.authenticate('example1','123456') - assert attributes.is_a?(Hash), "An hash was not returned" - assert_equal 'Example', attributes[:firstname] - assert_equal 'One', attributes[:lastname] - assert_equal 'example1@redmine.org', attributes[:mail] - assert_equal @auth.id, attributes[:auth_source_id] - attributes.keys.each do |attribute| - assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned" - end - end - end - - context 'with an invalid LDAP user' do - should 'return nil' do - assert_equal nil, @auth.authenticate('nouser','123456') - end - end - - context 'without a login' do - should 'return nil' do - assert_equal nil, @auth.authenticate('','123456') - end - end - - context 'without a password' do - should 'return nil' do - assert_equal nil, @auth.authenticate('edavis','') - end - end - - context 'without filter' do - should 'return any user' do - assert @auth.authenticate('example1','123456') - assert @auth.authenticate('edavis', '123456') - end - end - - context 'with filter' do - setup do - @auth.filter = "(mail=*@redmine.org)" - end - - should 'return user who matches the filter only' do - assert @auth.authenticate('example1','123456') - assert_nil @auth.authenticate('edavis', '123456') - end - end - end - - def test_authenticate_should_timeout - auth_source = AuthSourceLdap.find(1) - auth_source.timeout = 1 - def auth_source.initialize_ldap_con(*args); sleep(5); end - - assert_raise AuthSourceTimeoutException do - auth_source.authenticate 'example1', '123456' - end - end - else - puts '(Test LDAP server not configured)' - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/34/34f59b6c99dab380ca3ed7001d4ae58c9c6a2f50.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/34f59b6c99dab380ca3ed7001d4ae58c9c6a2f50.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,89 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 CustomFieldsController < ApplicationController + layout 'admin' + + before_filter :require_admin + before_filter :build_new_custom_field, :only => [:new, :create] + before_filter :find_custom_field, :only => [:edit, :update, :destroy] + accept_api_auth :index + + def index + respond_to do |format| + format.html { + @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name } + @tab = params[:tab] || 'IssueCustomField' + } + format.api { + @custom_fields = CustomField.all + } + end + end + + def new + end + + def create + if @custom_field.save + flash[:notice] = l(:notice_successful_create) + call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field) + redirect_to custom_fields_path(:tab => @custom_field.class.name) + else + render :action => 'new' + end + end + + def edit + end + + def update + if @custom_field.update_attributes(params[:custom_field]) + flash[:notice] = l(:notice_successful_update) + call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field) + redirect_to custom_fields_path(:tab => @custom_field.class.name) + else + render :action => 'edit' + end + end + + def destroy + begin + @custom_field.destroy + rescue + flash[:error] = l(:error_can_not_delete_custom_field) + end + redirect_to custom_fields_path(:tab => @custom_field.class.name) + end + + private + + def build_new_custom_field + @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field]) + if @custom_field.nil? + render_404 + else + @custom_field.default_value = nil + end + end + + def find_custom_field + @custom_field = CustomField.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/350a96c8e13497f31b6983c03aa5579b67c86870.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/350a96c8e13497f31b6983c03aa5579b67c86870.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,24 @@ +

    <%=l(:button_change_password)%>

    + +<%= error_messages_for 'user' %> + +<%= form_tag({}, :class => "tabular") do %> +
    +

    +<%= password_field_tag 'password', nil, :size => 25 %>

    + +

    +<%= password_field_tag 'new_password', nil, :size => 25 %> +<%= l(:text_caracters_minimum, :count => Setting.password_min_length) %>

    + +

    +<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>

    +
    +<%= submit_tag l(:button_apply) %> +<% end %> + +<% unless @user.must_change_passwd? %> +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> +<% end %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/351ed79f1a1eb0a05256bd9bbc96463418f0bb9b.svn-base --- a/.svn/pristine/35/351ed79f1a1eb0a05256bd9bbc96463418f0bb9b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -module Redmine - module Scm - class Base - class << self - - def all - @scms - end - - # Add a new SCM adapter and repository - def add(scm_name) - @scms ||= [] - @scms << scm_name - end - - # Remove a SCM adapter from Redmine's list of supported scms - def delete(scm_name) - @scms.delete(scm_name) - end - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/353b4ea784cce222db202d9a47753a1bd3685b81.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/353b4ea784cce222db202d9a47753a1bd3685b81.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,70 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 SearchHelper + def highlight_tokens(text, tokens) + return text unless text && tokens && !tokens.empty? + re_tokens = tokens.collect {|t| Regexp.escape(t)} + regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE + result = '' + text.split(regexp).each_with_index do |words, i| + if result.length > 1200 + # maximum length of the preview reached + result << '...' + break + end + words = words.mb_chars + if i.even? + result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words) + else + t = (tokens.index(words.downcase) || 0) % 4 + result << content_tag('span', h(words), :class => "highlight token-#{t}") + end + end + result.html_safe + end + + def type_label(t) + l("label_#{t.singularize}_plural", :default => t.to_s.humanize) + end + + def project_select_tag + options = [[l(:label_project_all), 'all']] + options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? + options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty? + options << [@project.name, ''] unless @project.nil? + label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") + + select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 + end + + def render_results_by_type(results_by_type) + links = [] + # Sorts types by results count + results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t| + c = results_by_type[t] + next if c == 0 + text = "#{type_label(t)} (#{c})" + links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only], + :all_words => params[:all_words], :scope => params[:scope], t => 1) + end + ('
      '.html_safe + + links.map {|link| content_tag('li', link)}.join(' ').html_safe + + '
    '.html_safe) unless links.empty? + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/358efe99a3fb53178835eb3b5414a8ad980d694b.svn-base --- a/.svn/pristine/35/358efe99a3fb53178835eb3b5414a8ad980d694b.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -class AddCommitAccessPermission < ActiveRecord::Migration - def self.up - Role.find(:all).select { |r| not r.builtin? }.each do |r| - r.add_permission!(:commit_access) - end - end - - def self.down - Role.find(:all).select { |r| not r.builtin? }.each do |r| - r.remove_permission!(:commit_access) - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/35a0a9f6553f729051c01dc102ed98a7a0151c3f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/35a0a9f6553f729051c01dc102ed98a7a0151c3f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,243 @@ +# encoding: utf-8 +# +# Helpers to sort tables using clickable column headers. +# +# Author: Stuart Rackham , March 2005. +# Jean-Philippe Lang, 2009 +# License: This source code is released under the MIT license. +# +# - Consecutive clicks toggle the column's sort order. +# - Sort state is maintained by a session hash entry. +# - CSS classes identify sort column and state. +# - Typically used in conjunction with the Pagination module. +# +# Example code snippets: +# +# Controller: +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update %w(first_name last_name) +# @items = Contact.find_all nil, sort_clause +# end +# +# Controller (using Pagination module): +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update %w(first_name last_name) +# @contact_pages, @items = paginate :contacts, +# :order_by => sort_clause, +# :per_page => 10 +# end +# +# View (table header in list.rhtml): +# +# +# +# <%= sort_header_tag('id', :title => 'Sort by contact ID') %> +# <%= sort_header_tag('last_name', :caption => 'Name') %> +# <%= sort_header_tag('phone') %> +# <%= sort_header_tag('address', :width => 200) %> +# +# +# +# - Introduces instance variables: @sort_default, @sort_criteria +# - Introduces param :sort +# + +module SortHelper + class SortCriteria + + def initialize + @criteria = [] + end + + def available_criteria=(criteria) + unless criteria.is_a?(Hash) + criteria = criteria.inject({}) {|h,k| h[k] = k; h} + end + @available_criteria = criteria + end + + def from_param(param) + @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]} + normalize! + end + + def criteria=(arg) + @criteria = arg + normalize! + end + + def to_param + @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',') + end + + # Returns an array of SQL fragments used to sort the list + def to_sql + sql = @criteria.collect do |k,o| + if s = @available_criteria[k] + (o ? s.to_a : s.to_a.collect {|c| append_desc(c)}) + end + end.flatten.compact + sql.blank? ? nil : sql + end + + def to_a + @criteria.dup + end + + def add!(key, asc) + @criteria.delete_if {|k,o| k == key} + @criteria = [[key, asc]] + @criteria + normalize! + end + + def add(*args) + r = self.class.new.from_param(to_param) + r.add!(*args) + r + end + + def first_key + @criteria.first && @criteria.first.first + end + + def first_asc? + @criteria.first && @criteria.first.last + end + + def empty? + @criteria.empty? + end + + private + + def normalize! + @criteria ||= [] + @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]} + @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria + @criteria.slice!(3) + self + end + + # Appends DESC to the sort criterion unless it has a fixed order + def append_desc(criterion) + if criterion =~ / (asc|desc)$/i + criterion + else + "#{criterion} DESC" + end + end + end + + def sort_name + controller_name + '_' + action_name + '_sort' + end + + # Initializes the default sort. + # Examples: + # + # sort_init 'name' + # sort_init 'id', 'desc' + # sort_init ['name', ['id', 'desc']] + # sort_init [['name', 'desc'], ['id', 'desc']] + # + def sort_init(*args) + case args.size + when 1 + @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]] + when 2 + @sort_default = [[args.first, args.last]] + else + raise ArgumentError + end + end + + # Updates the sort state. Call this in the controller prior to calling + # sort_clause. + # - criteria can be either an array or a hash of allowed keys + # + def sort_update(criteria, sort_name=nil) + sort_name ||= self.sort_name + @sort_criteria = SortCriteria.new + @sort_criteria.available_criteria = criteria + @sort_criteria.from_param(params[:sort] || session[sort_name]) + @sort_criteria.criteria = @sort_default if @sort_criteria.empty? + session[sort_name] = @sort_criteria.to_param + end + + # Clears the sort criteria session data + # + def sort_clear + session[sort_name] = nil + end + + # Returns an SQL sort clause corresponding to the current sort state. + # Use this to sort the controller's table items collection. + # + def sort_clause() + @sort_criteria.to_sql + end + + def sort_criteria + @sort_criteria + end + + # Returns a link which sorts by the named column. + # + # - column is the name of an attribute in the sorted record collection. + # - the optional caption explicitly specifies the displayed link text. + # - 2 CSS classes reflect the state of the link: sort and asc or desc + # + def sort_link(column, caption, default_order) + css, order = nil, default_order + + if column.to_s == @sort_criteria.first_key + if @sort_criteria.first_asc? + css = 'sort asc' + order = 'desc' + else + css = 'sort desc' + order = 'asc' + end + end + caption = column.to_s.humanize unless caption + + sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } + url_options = params.merge(sort_options) + + # Add project_id to url_options + url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) + + link_to_content_update(h(caption), url_options, :class => css) + end + + # Returns a table header tag with a sort link for the named column + # attribute. + # + # Options: + # :caption The displayed link name (defaults to titleized column name). + # :title The tag's 'title' attribute (defaults to 'Sort by :caption'). + # + # Other options hash entries generate additional table header tag attributes. + # + # Example: + # + # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %> + # + def sort_header_tag(column, options = {}) + caption = options.delete(:caption) || column.to_s.humanize + default_order = options.delete(:default_order) || 'asc' + options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title] + content_tag('th', sort_link(column, caption, default_order), options) + end +end + diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/35e3d4caedb7dd68a537eff356ac08671dc6c73f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/35e3d4caedb7dd68a537eff356ac08671dc6c73f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,108 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RolesController < ApplicationController + layout 'admin' + + before_filter :require_admin, :except => [:index, :show] + before_filter :require_admin_or_api_request, :only => [:index, :show] + before_filter :find_role, :only => [:show, :edit, :update, :destroy] + accept_api_auth :index, :show + + def index + respond_to do |format| + format.html { + @role_pages, @roles = paginate Role.sorted, :per_page => 25 + render :action => "index", :layout => false if request.xhr? + } + format.api { + @roles = Role.givable.all + } + end + end + + def show + respond_to do |format| + format.api + end + end + + def new + # Prefills the form with 'Non member' role permissions by default + @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions}) + if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy]) + @role.copy_from(@copy_from) + end + @roles = Role.sorted.all + end + + def create + @role = Role.new(params[:role]) + if request.post? && @role.save + # workflow copy + if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from])) + @role.workflow_rules.copy(copy_from) + end + flash[:notice] = l(:notice_successful_create) + redirect_to roles_path + else + @roles = Role.sorted.all + render :action => 'new' + end + end + + def edit + end + + def update + if request.put? and @role.update_attributes(params[:role]) + flash[:notice] = l(:notice_successful_update) + redirect_to roles_path + else + render :action => 'edit' + end + end + + def destroy + @role.destroy + redirect_to roles_path + rescue + flash[:error] = l(:error_can_not_remove_role) + redirect_to roles_path + end + + def permissions + @roles = Role.sorted.all + @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } + if request.post? + @roles.each do |role| + role.permissions = params[:permissions][role.id.to_s] + role.save + end + flash[:notice] = l(:notice_successful_update) + redirect_to roles_path + end + end + + private + + def find_role + @role = Role.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/35e7528b96f6bca4ea96cc9fa52b687ed6909cb9.svn-base --- a/.svn/pristine/35/35e7528b96f6bca4ea96cc9fa52b687ed6909cb9.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,219 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 RolesControllerTest < ActionController::TestCase - fixtures :roles, :users, :members, :member_roles, :workflows, :trackers - - def setup - @controller = RolesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - @request.session[:user_id] = 1 # admin - end - - def test_index - get :index - assert_response :success - assert_template 'index' - - assert_not_nil assigns(:roles) - assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles) - - assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' }, - :content => 'Manager' - end - - def test_new - get :new - assert_response :success - assert_template 'new' - end - - def test_new_with_copy - copy_from = Role.find(2) - - get :new, :copy => copy_from.id.to_s - assert_response :success - assert_template 'new' - - role = assigns(:role) - assert_equal copy_from.permissions, role.permissions - - assert_select 'form' do - # blank name - assert_select 'input[name=?][value=]', 'role[name]' - # edit_project permission checked - assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]' - # add_project permission not checked - assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]' - assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0 - # workflow copy selected - assert_select 'select[name=?]', 'copy_workflow_from' do - assert_select 'option[value=2][selected=selected]' - end - end - end - - def test_create_with_validaton_failure - post :create, :role => {:name => '', - :permissions => ['add_issues', 'edit_issues', 'log_time', ''], - :assignable => '0'} - - assert_response :success - assert_template 'new' - assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' } - end - - def test_create_without_workflow_copy - post :create, :role => {:name => 'RoleWithoutWorkflowCopy', - :permissions => ['add_issues', 'edit_issues', 'log_time', ''], - :assignable => '0'} - - assert_redirected_to '/roles' - role = Role.find_by_name('RoleWithoutWorkflowCopy') - assert_not_nil role - assert_equal [:add_issues, :edit_issues, :log_time], role.permissions - assert !role.assignable? - end - - def test_create_with_workflow_copy - post :create, :role => {:name => 'RoleWithWorkflowCopy', - :permissions => ['add_issues', 'edit_issues', 'log_time', ''], - :assignable => '0'}, - :copy_workflow_from => '1' - - assert_redirected_to '/roles' - role = Role.find_by_name('RoleWithWorkflowCopy') - assert_not_nil role - assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size - end - - def test_edit - get :edit, :id => 1 - assert_response :success - assert_template 'edit' - assert_equal Role.find(1), assigns(:role) - assert_select 'select[name=?]', 'role[issues_visibility]' - end - - def test_edit_anonymous - get :edit, :id => Role.anonymous.id - assert_response :success - assert_template 'edit' - assert_select 'select[name=?]', 'role[issues_visibility]', 0 - end - - def test_edit_invalid_should_respond_with_404 - get :edit, :id => 999 - assert_response 404 - end - - def test_update - put :update, :id => 1, - :role => {:name => 'Manager', - :permissions => ['edit_project', ''], - :assignable => '0'} - - assert_redirected_to '/roles' - role = Role.find(1) - assert_equal [:edit_project], role.permissions - end - - def test_update_with_failure - put :update, :id => 1, :role => {:name => ''} - assert_response :success - assert_template 'edit' - end - - def test_destroy - r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages]) - - delete :destroy, :id => r - assert_redirected_to '/roles' - assert_nil Role.find_by_id(r.id) - end - - def test_destroy_role_in_use - delete :destroy, :id => 1 - assert_redirected_to '/roles' - assert_equal 'This role is in use and cannot be deleted.', flash[:error] - assert_not_nil Role.find_by_id(1) - end - - def test_get_permissions - get :permissions - assert_response :success - assert_template 'permissions' - - assert_not_nil assigns(:roles) - assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles) - - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'permissions[3][]', - :value => 'add_issues', - :checked => 'checked' } - - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'permissions[3][]', - :value => 'delete_issues', - :checked => nil } - end - - def test_post_permissions - post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']} - assert_redirected_to '/roles' - - assert_equal [:edit_issues], Role.find(1).permissions - assert_equal [:add_issues, :delete_issues], Role.find(3).permissions - assert Role.find(2).permissions.empty? - end - - def test_clear_all_permissions - post :permissions, :permissions => { '0' => '' } - assert_redirected_to '/roles' - assert Role.find(1).permissions.empty? - end - - def test_move_highest - put :update, :id => 3, :role => {:move_to => 'highest'} - assert_redirected_to '/roles' - assert_equal 1, Role.find(3).position - end - - def test_move_higher - position = Role.find(3).position - put :update, :id => 3, :role => {:move_to => 'higher'} - assert_redirected_to '/roles' - assert_equal position - 1, Role.find(3).position - end - - def test_move_lower - position = Role.find(2).position - put :update, :id => 2, :role => {:move_to => 'lower'} - assert_redirected_to '/roles' - assert_equal position + 1, Role.find(2).position - end - - def test_move_lowest - put :update, :id => 2, :role => {:move_to => 'lowest'} - assert_redirected_to '/roles' - assert_equal Role.count, Role.find(2).position - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/35e81556cbc400d044f40ebe95f71b9bf32a7ed2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/35e81556cbc400d044f40ebe95f71b9bf32a7ed2.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,202 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 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 QueriesHelper + def filters_options_for_select(query) + options_for_select(filters_options(query)) + end + + def filters_options(query) + options = [[]] + options += query.available_filters.map do |field, field_options| + [field_options[:name], field] + end + end + + def query_filters_hidden_tags(query) + tags = ''.html_safe + query.filters.each do |field, options| + tags << hidden_field_tag("f[]", field, :id => nil) + tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil) + options[:values].each do |value| + tags << hidden_field_tag("v[#{field}][]", value, :id => nil) + end + end + tags + end + + def query_columns_hidden_tags(query) + tags = ''.html_safe + query.columns.each do |column| + tags << hidden_field_tag("c[]", column.name, :id => nil) + end + tags + end + + def query_hidden_tags(query) + query_filters_hidden_tags(query) + query_columns_hidden_tags(query) + end + + def available_block_columns_tags(query) + tags = ''.html_safe + query.available_block_columns.each do |column| + tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline') + end + tags + end + + def query_available_inline_columns_options(query) + (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]} + end + + def query_selected_inline_columns_options(query) + (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]} + end + + def render_query_columns_selection(query, options={}) + tag_name = (options[:name] || 'c') + '[]' + render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name} + end + + def column_header(column) + column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption, + :default_order => column.default_order) : + content_tag('th', h(column.caption)) + end + + def column_content(column, issue) + value = column.value(issue) + if value.is_a?(Array) + value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe + else + column_value(column, issue, value) + end + end + + def column_value(column, issue, value) + case column.name + when :id + link_to value, issue_path(issue) + when :subject + link_to value, issue_path(issue) + when :description + issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : '' + when :done_ratio + progress_bar(value, :width => '80px') + when :relations + other = value.other_issue(issue) + content_tag('span', + (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe, + :class => value.css_classes_for(issue)) + else + format_object(value) + end + end + + def csv_content(column, issue) + value = column.value(issue) + if value.is_a?(Array) + value.collect {|v| csv_value(column, issue, v)}.compact.join(', ') + else + csv_value(column, issue, value) + end + end + + def csv_value(column, issue, value) + case value.class.name + when 'Time' + format_time(value) + when 'Date' + format_date(value) + when 'Float' + sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator)) + when 'IssueRelation' + other = value.other_issue(issue) + l(value.label_for(issue)) + " ##{other.id}" + when 'TrueClass' + l(:general_text_Yes) + when 'FalseClass' + l(:general_text_No) + else + value.to_s + end + end + + def query_to_csv(items, query, options={}) + encoding = l(:general_csv_encoding) + columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns) + query.available_block_columns.each do |column| + if options[column.name].present? + columns << column + end + end + + export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| + # csv header fields + csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } + # csv lines + items.each do |item| + csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) } + end + end + export + end + + # Retrieve query from session or build a new query + def retrieve_query + if !params[:query_id].blank? + cond = "project_id IS NULL" + cond << " OR project_id = #{@project.id}" if @project + @query = IssueQuery.where(cond).find(params[:query_id]) + raise ::Unauthorized unless @query.visible? + @query.project = @project + session[:query] = {:id => @query.id, :project_id => @query.project_id} + sort_clear + elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) + # Give it a name, required to be valid + @query = IssueQuery.new(:name => "_") + @query.project = @project + @query.build_from_params(params) + session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names} + else + # retrieve from session + @query = nil + @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id] + @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) + @query.project = @project + end + end + + def retrieve_query_from_session + if session[:query] + if session[:query][:id] + @query = IssueQuery.find_by_id(session[:query][:id]) + return unless @query + else + @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) + end + if session[:query].has_key?(:project_id) + @query.project_id = session[:query][:project_id] + else + @query.project = @project + end + @query + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/35/35f480895683cc5d4622ecd0b8aa6794e388969c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/35f480895683cc5d4622ecd0b8aa6794e388969c.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,86 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 ContextMenusController < ApplicationController + helper :watchers + helper :issues + + before_filter :find_issues, :only => :issues + + def issues + if (@issues.size == 1) + @issue = @issues.first + end + @issue_ids = @issues.map(&:id).sort + + @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&) + + @can = {:edit => User.current.allowed_to?(:edit_issues, @projects), + :log_time => (@project && User.current.allowed_to?(:log_time, @project)), + :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)), + :move => (@project && User.current.allowed_to?(:move_issues, @project)), + :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => User.current.allowed_to?(:delete_issues, @projects) + } + if @project + if @issue + @assignables = @issue.assignable_users + else + @assignables = @project.assignable_users + end + @trackers = @project.trackers + else + #when multiple projects, we only keep the intersection of each set + @assignables = @projects.map(&:assignable_users).reduce(:&) + @trackers = @projects.map(&:trackers).reduce(:&) + end + @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&) + + @priorities = IssuePriority.active.reverse + @back = back_url + + @options_by_custom_field = {} + if @can[:edit] + custom_fields = @issues.map(&:available_custom_fields).reduce(:&).select do |f| + %w(bool list user version).include?(f.field_format) && !f.multiple? + end + custom_fields.each do |field| + values = field.possible_values_options(@projects) + if values.any? + @options_by_custom_field[field] = values + end + end + end + + @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&) + render :layout => false + end + + def time_entries + @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a + (render_404; return) unless @time_entries.present? + + @projects = @time_entries.collect(&:project).compact.uniq + @project = @projects.first if @projects.size == 1 + @activities = TimeEntryActivity.shared.active + @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects), + :delete => User.current.allowed_to?(:edit_time_entries, @projects) + } + @back = back_url + render :layout => false + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/36/367408f1f75021a71b939b14007872b6740672f3.svn-base --- a/.svn/pristine/36/367408f1f75021a71b939b14007872b6740672f3.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,136 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 ApiTest::VersionsTest < ActionController::IntegrationTest - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows, - :versions - - def setup - Setting.rest_api_enabled = '1' - end - - context "/projects/:project_id/versions" do - context "GET" do - should "return project versions" do - get '/projects/1/versions.xml' - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'versions', - :attributes => {:type => 'array'}, - :child => { - :tag => 'version', - :child => { - :tag => 'id', - :content => '2', - :sibling => { - :tag => 'name', - :content => '1.0' - } - } - } - end - end - - context "POST" do - should "create the version" do - assert_difference 'Version.count' do - post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith') - end - - version = Version.first(:order => 'id DESC') - assert_equal 'API test', version.name - - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} - end - - should "create the version with due date" do - assert_difference 'Version.count' do - post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith') - end - - version = Version.first(:order => 'id DESC') - assert_equal 'API test', version.name - assert_equal Date.parse('2012-01-24'), version.due_date - - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} - end - - context "with failure" do - should "return the errors" do - assert_no_difference('Version.count') do - post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith') - end - - assert_response :unprocessable_entity - assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"} - end - end - end - end - - context "/versions/:id" do - context "GET" do - should "return the version" do - get '/versions/2.xml' - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_select 'version' do - assert_select 'id', :text => '2' - assert_select 'name', :text => '1.0' - assert_select 'sharing', :text => 'none' - end - end - end - - context "PUT" do - should "update the version" do - put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith') - - assert_response :ok - assert_equal '', @response.body - assert_equal 'API update', Version.find(2).name - end - end - - context "DELETE" do - should "destroy the version" do - assert_difference 'Version.count', -1 do - delete '/versions/3.xml', {}, credentials('jsmith') - end - - assert_response :ok - assert_equal '', @response.body - assert_nil Version.find_by_id(3) - end - end - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/36/36f96aa6819066dbc18f6f0375ee24e3c2d52b96.svn-base --- a/.svn/pristine/36/36f96aa6819066dbc18f6f0375ee24e3c2d52b96.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,296 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2012 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 AccountController < ApplicationController - helper :custom_fields - include CustomFieldsHelper - - # prevents login action to be filtered by check_if_login_required application scope filter - skip_before_filter :check_if_login_required - - # Login request and validation - def login - if request.get? - logout_user - else - authenticate_user - end - rescue AuthSourceException => e - logger.error "An error occured when authenticating #{params[:username]}: #{e.message}" - render_error :message => e.message - end - - # Log out current user and redirect to welcome page - def logout - logout_user - redirect_to home_url - end - - # Lets user choose a new password - def lost_password - redirect_to(home_url) && return unless Setting.lost_password? - if params[:token] - @token = Token.find_by_action_and_value("recovery", params[:token].to_s) - if @token.nil? || @token.expired? - redirect_to home_url - return - end - @user = @token.user - unless @user && @user.active? - redirect_to home_url - return - end - if request.post? - @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] - if @user.save - @token.destroy - flash[:notice] = l(:notice_account_password_updated) - redirect_to signin_path - return - end - end - render :template => "account/password_recovery" - return - else - if request.post? - user = User.find_by_mail(params[:mail].to_s) - # user not found or not active - unless user && user.active? - flash.now[:error] = l(:notice_account_unknown_email) - return - end - # user cannot change its password - unless user.change_password_allowed? - flash.now[:error] = l(:notice_can_t_change_password) - return - end - # create a new token for password recovery - token = Token.new(:user => user, :action => "recovery") - if token.save - Mailer.lost_password(token).deliver - flash[:notice] = l(:notice_account_lost_email_sent) - redirect_to signin_path - return - end - end - end - end - - # User self-registration - def register - redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration] - if request.get? - session[:auth_source_registration] = nil - @user = User.new(:language => Setting.default_language) - else - user_params = params[:user] || {} - @user = User.new - @user.safe_attributes = user_params - @user.admin = false - @user.register - if session[:auth_source_registration] - @user.activate - @user.login = session[:auth_source_registration][:login] - @user.auth_source_id = session[:auth_source_registration][:auth_source_id] - if @user.save - session[:auth_source_registration] = nil - self.logged_user = @user - flash[:notice] = l(:notice_account_activated) - redirect_to :controller => 'my', :action => 'account' - end - else - @user.login = params[:user][:login] - unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank? - @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation] - end - - case Setting.self_registration - when '1' - register_by_email_activation(@user) - when '3' - register_automatically(@user) - else - register_manually_by_administrator(@user) - end - end - end - end - - # Token based account activation - def activate - redirect_to(home_url) && return unless Setting.self_registration? && params[:token] - token = Token.find_by_action_and_value('register', params[:token]) - redirect_to(home_url) && return unless token and !token.expired? - user = token.user - redirect_to(home_url) && return unless user.registered? - user.activate - if user.save - token.destroy - flash[:notice] = l(:notice_account_activated) - end - redirect_to signin_path - end - - private - - def authenticate_user - if Setting.openid? && using_open_id? - open_id_authenticate(params[:openid_url]) - else - password_authentication - end - end - - def password_authentication - user = User.try_to_login(params[:username], params[:password]) - - if user.nil? - invalid_credentials - elsif user.new_record? - onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id }) - else - # Valid user - successful_authentication(user) - end - end - - def open_id_authenticate(openid_url) - authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration| - if result.successful? - user = User.find_or_initialize_by_identity_url(identity_url) - if user.new_record? - # Self-registration off - redirect_to(home_url) && return unless Setting.self_registration? - - # Create on the fly - user.login = registration['nickname'] unless registration['nickname'].nil? - user.mail = registration['email'] unless registration['email'].nil? - user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil? - user.random_password - user.register - - case Setting.self_registration - when '1' - register_by_email_activation(user) do - onthefly_creation_failed(user) - end - when '3' - register_automatically(user) do - onthefly_creation_failed(user) - end - else - register_manually_by_administrator(user) do - onthefly_creation_failed(user) - end - end - else - # Existing record - if user.active? - successful_authentication(user) - else - account_pending - end - end - end - end - end - - def successful_authentication(user) - logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}" - # Valid user - self.logged_user = user - # generate a key and set cookie if autologin - if params[:autologin] && Setting.autologin? - set_autologin_cookie(user) - end - call_hook(:controller_account_success_authentication_after, {:user => user }) - redirect_back_or_default :controller => 'my', :action => 'page' - end - - def set_autologin_cookie(user) - token = Token.create(:user => user, :action => 'autologin') - cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin' - cookie_options = { - :value => token.value, - :expires => 1.year.from_now, - :path => (Redmine::Configuration['autologin_cookie_path'] || '/'), - :secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false), - :httponly => true - } - cookies[cookie_name] = cookie_options - end - - # Onthefly creation failed, display the registration form to fill/fix attributes - def onthefly_creation_failed(user, auth_source_options = { }) - @user = user - session[:auth_source_registration] = auth_source_options unless auth_source_options.empty? - render :action => 'register' - end - - def invalid_credentials - logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}" - flash.now[:error] = l(:notice_account_invalid_creditentials) - end - - # Register a user for email activation. - # - # Pass a block for behavior when a user fails to save - def register_by_email_activation(user, &block) - token = Token.new(:user => user, :action => "register") - if user.save and token.save - Mailer.register(token).deliver - flash[:notice] = l(:notice_account_register_done) - redirect_to signin_path - else - yield if block_given? - end - end - - # Automatically register a user - # - # Pass a block for behavior when a user fails to save - def register_automatically(user, &block) - # Automatic activation - user.activate - user.last_login_on = Time.now - if user.save - self.logged_user = user - flash[:notice] = l(:notice_account_activated) - redirect_to :controller => 'my', :action => 'account' - else - yield if block_given? - end - end - - # Manual activation by the administrator - # - # Pass a block for behavior when a user fails to save - def register_manually_by_administrator(user, &block) - if user.save - # Sends an email to the administrators - Mailer.account_activation_request(user).deliver - account_pending - else - yield if block_given? - end - end - - def account_pending - flash[:notice] = l(:notice_account_pending) - redirect_to signin_path - end -end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/37/371760b9ad0d0b40ef98e5a2a6cefda71c92b15e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/37/371760b9ad0d0b40ef98e5a2a6cefda71c92b15e.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,4 @@ +
    +<%= link_to l(:button_copy), {:action => 'copy'}, :class => 'icon icon-copy' %> +<%= link_to l(:field_summary), {:action => 'index'}, :class => 'icon icon-summary' %> +
    diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/37/3751ea61d6afde6d3676ff52f3635b315c4f2a51.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/37/3751ea61d6afde6d3676ff52f3635b315c4f2a51.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,41 @@ +<%= form_tag({}) do -%> +<%= hidden_field_tag 'back_url', url_for(params) %> +
    + + + + + <% @query.inline_columns.each do |column| %> + <%= column_header(column) %> + <% end %> + + + + +<% entries.each do |entry| -%> + hascontextmenu"> + + <%= raw @query.inline_columns.map {|column| ""}.join %> + + +<% end -%> + +
    + <%= link_to image_tag('toggle_check.png'), + {}, + :onclick => 'toggleIssuesSelection(this); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> +
    <%= check_box_tag("ids[]", entry.id, false, :id => nil) %>#{column_content(column, entry)} + <% if entry.editable_by?(User.current) -%> + <%= link_to image_tag('edit.png'), edit_time_entry_path(entry), + :title => l(:button_edit) %> + <%= link_to image_tag('delete.png'), time_entry_path(entry), + :data => {:confirm => l(:text_are_you_sure)}, + :method => :delete, + :title => l(:button_delete) %> + <% end -%> +
    +
    +<% end -%> + +<%= context_menu time_entries_context_menu_path %> diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/37/3777bfd256635d6f1a4b09b877d82ce7f1424b2a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/37/3777bfd256635d6f1a4b09b877d82ce7f1424b2a.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,58 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RoutingDocumentsTest < ActionController::IntegrationTest + def test_documents_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/567/documents" }, + { :controller => 'documents', :action => 'index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/documents/new" }, + { :controller => 'documents', :action => 'new', :project_id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/projects/567/documents" }, + { :controller => 'documents', :action => 'create', :project_id => '567' } + ) + end + + def test_documents + assert_routing( + { :method => 'get', :path => "/documents/22" }, + { :controller => 'documents', :action => 'show', :id => '22' } + ) + assert_routing( + { :method => 'get', :path => "/documents/22/edit" }, + { :controller => 'documents', :action => 'edit', :id => '22' } + ) + assert_routing( + { :method => 'put', :path => "/documents/22" }, + { :controller => 'documents', :action => 'update', :id => '22' } + ) + assert_routing( + { :method => 'delete', :path => "/documents/22" }, + { :controller => 'documents', :action => 'destroy', :id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/documents/22/add_attachment" }, + { :controller => 'documents', :action => 'add_attachment', :id => '22' } + ) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/37/37daad43f5c4a3d690d4d19284c0c5d27ead44dc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/37/37daad43f5c4a3d690d4d19284c0c5d27ead44dc.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,3998 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssuesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries, + :repositories, + :changesets + + include Redmine::I18n + + def setup + User.current = nil + end + + def test_index + with_settings :default_language => "en" do + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_nil assigns(:project) + + # links to visible issues + assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/ + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + # private projects hidden + assert_select 'a[href=/issues/6]', 0 + assert_select 'a[href=/issues/4]', 0 + # project column + assert_select 'th', :text => /Project/ + end + end + + def test_index_should_not_list_issues_when_module_disabled + EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1") + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_nil assigns(:project) + + assert_select 'a[href=/issues/1]', 0 + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + end + + def test_index_should_list_visible_issues_only + get :index, :per_page => 100 + assert_response :success + assert_not_nil assigns(:issues) + assert_nil assigns(:issues).detect {|issue| !issue.visible?} + end + + def test_index_with_project + Setting.display_subprojects_issues = 0 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/ + assert_select 'a[href=/issues/5]', 0 + end + + def test_index_with_project_and_subprojects + Setting.display_subprojects_issues = 1 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/ + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + assert_select 'a[href=/issues/6]', 0 + end + + def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission + @request.session[:user_id] = 2 + Setting.display_subprojects_issues = 1 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/ + assert_select 'a[href=/issues/5]', :text => /Subproject issue/ + assert_select 'a[href=/issues/6]', :text => /Issue of a private subproject/ + end + + def test_index_with_project_and_default_filter + get :index, :project_id => 1, :set_filter => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + # default filter + assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters) + end + + def test_index_with_project_and_filter + get :index, :project_id => 1, :set_filter => 1, + :f => ['tracker_id'], + :op => {'tracker_id' => '='}, + :v => {'tracker_id' => ['1']} + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters) + end + + def test_index_with_short_filters + to_test = { + 'status_id' => { + 'o' => { :op => 'o', :values => [''] }, + 'c' => { :op => 'c', :values => [''] }, + '7' => { :op => '=', :values => ['7'] }, + '7|3|4' => { :op => '=', :values => ['7', '3', '4'] }, + '=7' => { :op => '=', :values => ['7'] }, + '!3' => { :op => '!', :values => ['3'] }, + '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }}, + 'subject' => { + 'This is a subject' => { :op => '=', :values => ['This is a subject'] }, + 'o' => { :op => '=', :values => ['o'] }, + '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] }, + '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }}, + 'tracker_id' => { + '3' => { :op => '=', :values => ['3'] }, + '=3' => { :op => '=', :values => ['3'] }}, + 'start_date' => { + '2011-10-12' => { :op => '=', :values => ['2011-10-12'] }, + '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] }, + '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, + '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] }, + '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] }, + ' { :op => ' ['2'] }, + '>t+2' => { :op => '>t+', :values => ['2'] }, + 't+2' => { :op => 't+', :values => ['2'] }, + 't' => { :op => 't', :values => [''] }, + 'w' => { :op => 'w', :values => [''] }, + '>t-2' => { :op => '>t-', :values => ['2'] }, + ' { :op => ' ['2'] }, + 't-2' => { :op => 't-', :values => ['2'] }}, + 'created_on' => { + '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, + ' { :op => ' ['2'] }, + '>t-2' => { :op => '>t-', :values => ['2'] }, + 't-2' => { :op => 't-', :values => ['2'] }}, + 'cf_1' => { + 'c' => { :op => '=', :values => ['c'] }, + '!c' => { :op => '!', :values => ['c'] }, + '!*' => { :op => '!*', :values => [''] }, + '*' => { :op => '*', :values => [''] }}, + 'estimated_hours' => { + '=13.4' => { :op => '=', :values => ['13.4'] }, + '>=45' => { :op => '>=', :values => ['45'] }, + '<=125' => { :op => '<=', :values => ['125'] }, + '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] }, + '!*' => { :op => '!*', :values => [''] }, + '*' => { :op => '*', :values => [''] }} + } + + default_filter = { 'status_id' => {:operator => 'o', :values => [''] }} + + to_test.each do |field, expression_and_expected| + expression_and_expected.each do |filter_expression, expected| + + get :index, :set_filter => 1, field => filter_expression + + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + assert query.has_filter?(field) + assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters) + end + end + end + + def test_index_with_project_and_empty_filters + get :index, :project_id => 1, :set_filter => 1, :fields => [''] + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + + query = assigns(:query) + assert_not_nil query + # no filter + assert_equal({}, query.filters) + end + + def test_index_with_project_custom_field_filter + field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo') + CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo') + filter_name = "project.cf_#{field.id}" + @request.session[:user_id] = 1 + + get :index, :set_filter => 1, + :f => [filter_name], + :op => {filter_name => '='}, + :v => {filter_name => ['Foo']} + assert_response :success + assert_template 'index' + assert_equal [3, 5], assigns(:issues).map(&:project_id).uniq.sort + end + + def test_index_with_query + get :index, :project_id => 1, :query_id => 5 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_nil assigns(:issue_count_by_group) + end + + def test_index_with_query_grouped_by_tracker + get :index, :project_id => 1, :query_id => 6 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + end + + def test_index_with_query_grouped_by_list_custom_field + get :index, :project_id => 1, :query_id => 9 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + end + + def test_index_with_query_grouped_by_user_custom_field + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') + + get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}" + assert_response :success + + assert_select 'tr.group', 3 + assert_select 'tr.group' do + assert_select 'a', :text => 'John Smith' + assert_select 'span.count', :text => '1' + end + assert_select 'tr.group' do + assert_select 'a', :text => 'Dave Lopper' + assert_select 'span.count', :text => '2' + end + end + + def test_index_with_query_grouped_by_tracker_in_normal_order + 3.times {|i| Issue.generate!(:tracker_id => (i + 1))} + + get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc' + assert_response :success + + trackers = assigns(:issues).map(&:tracker).uniq + assert_equal [1, 2, 3], trackers.map(&:id) + end + + def test_index_with_query_grouped_by_tracker_in_reverse_order + 3.times {|i| Issue.generate!(:tracker_id => (i + 1))} + + get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc' + assert_response :success + + trackers = assigns(:issues).map(&:tracker).uniq + assert_equal [3, 2, 1], trackers.map(&:id) + end + + def test_index_with_query_id_and_project_id_should_set_session_query + get :index, :project_id => 1, :query_id => 4 + assert_response :success + assert_kind_of Hash, session[:query] + assert_equal 4, session[:query][:id] + assert_equal 1, session[:query][:project_id] + end + + def test_index_with_invalid_query_id_should_respond_404 + get :index, :project_id => 1, :query_id => 999 + assert_response 404 + end + + def test_index_with_cross_project_query_in_session_should_show_project_issues + q = IssueQuery.create!(:name => "test", :user_id => 2, :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil) + @request.session[:query] = {:id => q.id, :project_id => 1} + + with_settings :display_subprojects_issues => '0' do + get :index, :project_id => 1 + end + assert_response :success + assert_not_nil assigns(:query) + assert_equal q.id, assigns(:query).id + assert_equal 1, assigns(:query).project_id + assert_equal [1], assigns(:issues).map(&:project_id).uniq + end + + def test_private_query_should_not_be_available_to_other_users + q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil) + @request.session[:user_id] = 3 + + get :index, :query_id => q.id + assert_response 403 + end + + def test_private_query_should_be_available_to_its_user + q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil) + @request.session[:user_id] = 2 + + get :index, :query_id => q.id + assert_response :success + end + + def test_public_query_should_be_available_to_other_users + q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PUBLIC, :project => nil) + @request.session[:user_id] = 3 + + get :index, :query_id => q.id + assert_response :success + end + + def test_index_should_omit_page_param_in_export_links + get :index, :page => 2 + assert_response :success + assert_select 'a.atom[href=/issues.atom]' + assert_select 'a.csv[href=/issues.csv]' + assert_select 'a.pdf[href=/issues.pdf]' + assert_select 'form#csv-export-form[action=/issues.csv]' + end + + def test_index_should_not_warn_when_not_exceeding_export_limit + with_settings :issues_export_limit => 200 do + get :index + assert_select '#csv-export-options p.icon-warning', 0 + end + end + + def test_index_should_warn_when_exceeding_export_limit + with_settings :issues_export_limit => 2 do + get :index + assert_select '#csv-export-options p.icon-warning', :text => %r{limit: 2} + end + end + + def test_index_csv + get :index, :format => 'csv' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv; header=present', @response.content_type + assert @response.body.starts_with?("#,") + lines = @response.body.chomp.split("\n") + assert_equal assigns(:query).columns.size, lines[0].split(',').size + end + + def test_index_csv_with_project + get :index, :project_id => 1, :format => 'csv' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv; header=present', @response.content_type + end + + def test_index_csv_with_description + Issue.generate!(:description => 'test_index_csv_with_description') + + with_settings :default_language => 'en' do + get :index, :format => 'csv', :description => '1' + assert_response :success + assert_not_nil assigns(:issues) + end + + assert_equal 'text/csv; header=present', response.content_type + headers = response.body.chomp.split("\n").first.split(',') + assert_include 'Description', headers + assert_include 'test_index_csv_with_description', response.body + end + + def test_index_csv_with_spent_time_column + issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2) + TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today) + + get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours) + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + assert_include "#{issue.id},#{issue.subject},7.33", lines + end + + def test_index_csv_with_all_columns + get :index, :format => 'csv', :columns => 'all' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv; header=present', @response.content_type + assert_match /\A#,/, response.body + lines = response.body.chomp.split("\n") + assert_equal assigns(:query).available_inline_columns.size, lines[0].split(',').size + end + + def test_index_csv_with_multi_column_field + CustomField.find(1).update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + get :index, :format => 'csv', :columns => 'all' + assert_response :success + lines = @response.body.chomp.split("\n") + assert lines.detect {|line| line.include?('"MySQL, Oracle"')} + end + + def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator + field = IssueCustomField.create!(:name => 'Float', :is_for_all => true, :tracker_ids => [1], :field_format => 'float') + issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id => '185.6'}) + + with_settings :default_language => 'fr' do + get :index, :format => 'csv', :columns => 'all' + assert_response :success + issue_line = response.body.chomp.split("\n").map {|line| line.split(';')}.detect {|line| line[0]==issue.id.to_s} + assert_include '185,60', issue_line + end + + with_settings :default_language => 'en' do + get :index, :format => 'csv', :columns => 'all' + assert_response :success + issue_line = response.body.chomp.split("\n").map {|line| line.split(',')}.detect {|line| line[0]==issue.id.to_s} + assert_include '185.60', issue_line + end + end + + def test_index_csv_big_5 + with_settings :default_language => "zh-TW" do + str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88" + str_big5 = "\xa4@\xa4\xeb" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + str_big5.force_encoding('Big5') + end + issue = Issue.generate!(:subject => str_utf8) + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str_utf8], + :format => 'csv' + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + s1 = "\xaa\xac\xbaA" + if str_utf8.respond_to?(:force_encoding) + s1.force_encoding('Big5') + end + assert_include s1, lines[0] + assert_include str_big5, lines[1] + end + end + + def test_index_csv_cannot_convert_should_be_replaced_big_5 + with_settings :default_language => "zh-TW" do + str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85" + if str_utf8.respond_to?(:force_encoding) + str_utf8.force_encoding('UTF-8') + end + issue = Issue.generate!(:subject => str_utf8) + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str_utf8], + :c => ['status', 'subject'], + :format => 'csv', + :set_filter => 1 + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + s1 = "\xaa\xac\xbaA" # status + if str_utf8.respond_to?(:force_encoding) + s1.force_encoding('Big5') + end + assert lines[0].include?(s1) + s2 = lines[1].split(",")[2] + if s1.respond_to?(:force_encoding) + s3 = "\xa5H?" # subject + s3.force_encoding('Big5') + assert_equal s3, s2 + elsif RUBY_PLATFORM == 'java' + assert_equal "??", s2 + else + assert_equal "\xa5H???", s2 + end + end + end + + def test_index_csv_tw + with_settings :default_language => "zh-TW" do + str1 = "test_index_csv_tw" + issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5') + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str1], + :c => ['estimated_hours', 'subject'], + :format => 'csv', + :set_filter => 1 + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + assert_equal "#{issue.id},1234.50,#{str1}", lines[1] + end + end + + def test_index_csv_fr + with_settings :default_language => "fr" do + str1 = "test_index_csv_fr" + issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5') + + get :index, :project_id => 1, + :f => ['subject'], + :op => '=', :values => [str1], + :c => ['estimated_hours', 'subject'], + :format => 'csv', + :set_filter => 1 + assert_equal 'text/csv; header=present', @response.content_type + lines = @response.body.chomp.split("\n") + assert_equal "#{issue.id};1234,50;#{str1}", lines[1] + end + end + + def test_index_pdf + ["en", "zh", "zh-TW", "ja", "ko"].each do |lang| + with_settings :default_language => lang do + + get :index + assert_response :success + assert_template 'index' + + if lang == "ja" + if RUBY_PLATFORM != 'java' + assert_equal "CP932", l(:general_pdf_encoding) + end + if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932" + next + end + end + + get :index, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type + + get :index, :project_id => 1, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type + + get :index, :project_id => 1, :query_id => 6, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type + end + end + end + + def test_index_pdf_with_query_grouped_by_list_custom_field + get :index, :project_id => 1, :query_id => 9, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + assert_equal 'application/pdf', @response.content_type + end + + def test_index_atom + get :index, :project_id => 'ecookbook', :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_equal 'application/atom+xml', response.content_type + + assert_select 'feed' do + assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom' + assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues' + assert_select 'entry link[href=?]', 'http://test.host/issues/1' + end + end + + def test_index_sort + get :index, :sort => 'tracker,id:desc' + assert_response :success + + sort_params = @request.session['issues_index_sort'] + assert sort_params.is_a?(String) + assert_equal 'tracker,id:desc', sort_params + + issues = assigns(:issues) + assert_not_nil issues + assert !issues.empty? + assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id) + end + + def test_index_sort_by_field_not_included_in_columns + Setting.issue_list_default_columns = %w(subject author) + get :index, :sort => 'tracker' + end + + def test_index_sort_by_assigned_to + get :index, :sort => 'assigned_to' + assert_response :success + assignees = assigns(:issues).collect(&:assigned_to).compact + assert_equal assignees.sort, assignees + end + + def test_index_sort_by_assigned_to_desc + get :index, :sort => 'assigned_to:desc' + assert_response :success + assignees = assigns(:issues).collect(&:assigned_to).compact + assert_equal assignees.sort.reverse, assignees + end + + def test_index_group_by_assigned_to + get :index, :group_by => 'assigned_to', :sort => 'priority' + assert_response :success + end + + def test_index_sort_by_author + get :index, :sort => 'author' + assert_response :success + authors = assigns(:issues).collect(&:author) + assert_equal authors.sort, authors + end + + def test_index_sort_by_author_desc + get :index, :sort => 'author:desc' + assert_response :success + authors = assigns(:issues).collect(&:author) + assert_equal authors.sort.reverse, authors + end + + def test_index_group_by_author + get :index, :group_by => 'author', :sort => 'priority' + assert_response :success + end + + def test_index_sort_by_spent_hours + get :index, :sort => 'spent_hours:desc' + assert_response :success + hours = assigns(:issues).collect(&:spent_hours) + assert_equal hours.sort.reverse, hours + end + + def test_index_sort_by_user_custom_field + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') + + get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id" + assert_response :success + + assert_equal [2, 3, 1], assigns(:issues).select {|issue| issue.custom_field_value(cf).present?}.map(&:id) + end + + def test_index_with_columns + columns = ['tracker', 'subject', 'assigned_to'] + get :index, :set_filter => 1, :c => columns + assert_response :success + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal columns, query.column_names.map(&:to_s) + + # columns should be stored in session + assert_kind_of Hash, session[:query] + assert_kind_of Array, session[:query][:column_names] + assert_equal columns, session[:query][:column_names].map(&:to_s) + + # ensure only these columns are kept in the selected columns list + assert_select 'select#selected_columns option' do + assert_select 'option', 3 + assert_select 'option[value=tracker]' + assert_select 'option[value=project]', 0 + end + end + + def test_index_without_project_should_implicitly_add_project_column_to_default_columns + Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to'] + get :index, :set_filter => 1 + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal [:id, :project, :tracker, :subject, :assigned_to], query.columns.map(&:name) + end + + def test_index_without_project_and_explicit_default_columns_should_not_add_project_column + Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to'] + columns = ['id', 'tracker', 'subject', 'assigned_to'] + get :index, :set_filter => 1, :c => columns + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal columns.map(&:to_sym), query.columns.map(&:name) + end + + def test_index_with_custom_field_column + columns = %w(tracker subject cf_2) + get :index, :set_filter => 1, :c => columns + assert_response :success + + # query should use specified columns + query = assigns(:query) + assert_kind_of IssueQuery, query + assert_equal columns, query.column_names.map(&:to_s) + + assert_select 'table.issues td.cf_2.string' + end + + def test_index_with_multi_custom_field_column + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + get :index, :set_filter => 1, :c => %w(tracker subject cf_1) + assert_response :success + + assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle' + end + + def test_index_with_multi_user_custom_field_column + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + issue = Issue.find(1) + issue.custom_field_values = {field.id => ['2', '3']} + issue.save! + + get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"] + assert_response :success + + assert_select "table.issues td.cf_#{field.id}" do + assert_select 'a', 2 + assert_select 'a[href=?]', '/users/2', :text => 'John Smith' + assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper' + end + end + + def test_index_with_date_column + with_settings :date_format => '%d/%m/%Y' do + Issue.find(1).update_attribute :start_date, '1987-08-24' + get :index, :set_filter => 1, :c => %w(start_date) + assert_select "table.issues td.start_date", :text => '24/08/1987' + end + end + + def test_index_with_done_ratio_column + Issue.find(1).update_attribute :done_ratio, 40 + get :index, :set_filter => 1, :c => %w(done_ratio) + assert_select 'table.issues td.done_ratio' do + assert_select 'table.progress' do + assert_select 'td.closed[style=?]', 'width: 40%;' + end + end + end + + def test_index_with_spent_hours_column + get :index, :set_filter => 1, :c => %w(subject spent_hours) + assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00' + end + + def test_index_should_not_show_spent_hours_column_without_permission + Role.anonymous.remove_permission! :view_time_entries + get :index, :set_filter => 1, :c => %w(subject spent_hours) + assert_select 'td.spent_hours', 0 + end + + def test_index_with_fixed_version_column + get :index, :set_filter => 1, :c => %w(fixed_version) + assert_select 'table.issues td.fixed_version' do + assert_select 'a[href=?]', '/versions/2', :text => '1.0' + end + end + + def test_index_with_relations_column + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1)) + IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11)) + IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2)) + + get :index, :set_filter => 1, :c => %w(subject relations) + assert_response :success + assert_select "tr#issue-1 td.relations" do + assert_select "span", 3 + assert_select "span", :text => "Related to #7" + assert_select "span", :text => "Related to #8" + assert_select "span", :text => "Blocks #11" + end + assert_select "tr#issue-2 td.relations" do + assert_select "span", 1 + assert_select "span", :text => "Blocked by #12" + end + assert_select "tr#issue-3 td.relations" do + assert_select "span", 0 + end + + get :index, :set_filter => 1, :c => %w(relations), :format => 'csv' + assert_response :success + assert_equal 'text/csv; header=present', response.content_type + lines = response.body.chomp.split("\n") + assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines + assert_include '2,Blocked by #12', lines + assert_include '3,""', lines + + get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', response.content_type + end + + def test_index_with_description_column + get :index, :set_filter => 1, :c => %w(subject description) + + assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject + assert_select 'td.description[colspan=3]', :text => 'Unable to print recipes' + + get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', response.content_type + end + + def test_index_send_html_if_query_is_invalid + get :index, :f => ['start_date'], :op => {:start_date => '='} + assert_equal 'text/html', @response.content_type + assert_template 'index' + end + + def test_index_send_nothing_if_query_is_invalid + get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv' + assert_equal 'text/csv', @response.content_type + assert @response.body.blank? + end + + def test_show_by_anonymous + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_equal Issue.find(1), assigns(:issue) + assert_select 'div.issue div.description', :text => /Unable to print recipes/ + # anonymous role is allowed to add a note + assert_select 'form#issue-form' do + assert_select 'fieldset' do + assert_select 'legend', :text => 'Notes' + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + assert_select 'title', :text => "Bug #1: #{ESCAPED_UCANT} print recipes - eCookbook - Redmine" + end + + def test_show_by_manager + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + assert_select 'a', :text => /Quote/ + assert_select 'form#issue-form' do + assert_select 'fieldset' do + assert_select 'legend', :text => 'Change properties' + assert_select 'input[name=?]', 'issue[subject]' + end + assert_select 'fieldset' do + assert_select 'legend', :text => 'Log time' + assert_select 'input[name=?]', 'time_entry[hours]' + end + assert_select 'fieldset' do + assert_select 'legend', :text => 'Notes' + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + end + + def test_show_should_display_update_form + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]' + assert_select 'select[name=?]', 'issue[project_id]' + assert_select 'select[name=?]', 'issue[tracker_id]' + assert_select 'input[name=?]', 'issue[subject]' + assert_select 'textarea[name=?]', 'issue[description]' + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]' + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]' + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]' + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]' + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?]', 'issue[custom_field_values][2]' + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + def test_show_should_display_update_form_with_minimal_permissions + Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes] + WorkflowTransition.delete_all :role_id => 1 + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]', 0 + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]', 0 + assert_select 'input[name=?]', 'issue[subject]', 0 + assert_select 'textarea[name=?]', 'issue[description]', 0 + assert_select 'select[name=?]', 'issue[status_id]', 0 + assert_select 'select[name=?]', 'issue[priority_id]', 0 + assert_select 'select[name=?]', 'issue[assigned_to_id]', 0 + assert_select 'select[name=?]', 'issue[category_id]', 0 + assert_select 'select[name=?]', 'issue[fixed_version_id]', 0 + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + assert_select 'input[name=?]', 'issue[start_date]', 0 + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'select[name=?]', 'issue[done_ratio]', 0 + assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0 + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + def test_show_should_display_update_form_with_workflow_permissions + Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes] + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]', 0 + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]', 0 + assert_select 'input[name=?]', 'issue[subject]', 0 + assert_select 'textarea[name=?]', 'issue[description]', 0 + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]', 0 + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]', 0 + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + assert_select 'input[name=?]', 'issue[start_date]', 0 + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0 + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + assert_select 'textarea[name=?]', 'issue[notes]' + end + end + + def test_show_should_not_display_update_form_without_permissions + Role.find(1).update_attribute :permissions, [:view_issues] + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form', 0 + end + + def test_update_form_should_not_display_inactive_enumerations + assert !IssuePriority.find(15).active? + + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response :success + + assert_select 'form#issue-form' do + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=4]' + assert_select 'option[value=15]', 0 + end + end + end + + def test_update_form_should_allow_attachment_upload + @request.session[:user_id] = 2 + get :show, :id => 1 + + assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do + assert_select 'input[type=file][name=?]', 'attachments[dummy][file]' + end + end + + def test_show_should_deny_anonymous_access_without_permission + Role.anonymous.remove_permission!(:view_issues) + get :show, :id => 1 + assert_response :redirect + end + + def test_show_should_deny_anonymous_access_to_private_issue + Issue.where(:id => 1).update_all(["is_private = ?", true]) + get :show, :id => 1 + assert_response :redirect + end + + def test_show_should_deny_non_member_access_without_permission + Role.non_member.remove_permission!(:view_issues) + @request.session[:user_id] = 9 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_deny_non_member_access_to_private_issue + Issue.where(:id => 1).update_all(["is_private = ?", true]) + @request.session[:user_id] = 9 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_deny_member_access_without_permission + Role.find(1).remove_permission!(:view_issues) + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_deny_member_access_to_private_issue_without_permission + Issue.where(:id => 1).update_all(["is_private = ?", true]) + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response 403 + end + + def test_show_should_allow_author_access_to_private_issue + Issue.where(:id => 1).update_all(["is_private = ?, author_id = 3", true]) + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response :success + end + + def test_show_should_allow_assignee_access_to_private_issue + Issue.where(:id => 1).update_all(["is_private = ?, assigned_to_id = 3", true]) + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response :success + end + + def test_show_should_allow_member_access_to_private_issue_with_permission + Issue.where(:id => 1).update_all(["is_private = ?", true]) + User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all' + @request.session[:user_id] = 3 + get :show, :id => 1 + assert_response :success + end + + def test_show_should_not_disclose_relations_to_invisible_issues + Setting.cross_project_issue_relations = '1' + IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates') + # Relation to a private project issue + IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates') + + get :show, :id => 1 + assert_response :success + + assert_select 'div#relations' do + assert_select 'a', :text => /#2$/ + assert_select 'a', :text => /#4$/, :count => 0 + end + end + + def test_show_should_list_subtasks + Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue') + + get :show, :id => 1 + assert_response :success + + assert_select 'div#issue_tree' do + assert_select 'td.subject', :text => /Child Issue/ + end + end + + def test_show_should_list_parents + issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue') + + get :show, :id => issue.id + assert_response :success + + assert_select 'div.subject' do + assert_select 'h3', 'Child Issue' + assert_select 'a[href=/issues/1]' + end + end + + def test_show_should_not_display_prev_next_links_without_query_in_session + get :show, :id => 1 + assert_response :success + assert_nil assigns(:prev_issue_id) + assert_nil assigns(:next_issue_id) + + assert_select 'div.next-prev-links', 0 + end + + def test_show_should_display_prev_next_links_with_query_in_session + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil} + @request.session['issues_index_sort'] = 'id' + + with_settings :display_subprojects_issues => '0' do + get :show, :id => 3 + end + + assert_response :success + # Previous and next issues for all projects + assert_equal 2, assigns(:prev_issue_id) + assert_equal 5, assigns(:next_issue_id) + + count = Issue.open.visible.count + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/2]', :text => /Previous/ + assert_select 'a[href=/issues/5]', :text => /Next/ + assert_select 'span.position', :text => "3 of #{count}" + end + end + + def test_show_should_display_prev_next_links_with_saved_query_in_session + query = IssueQuery.create!(:name => 'test', :visibility => IssueQuery::VISIBILITY_PUBLIC, :user_id => 1, + :filters => {'status_id' => {:values => ['5'], :operator => '='}}, + :sort_criteria => [['id', 'asc']]) + @request.session[:query] = {:id => query.id, :project_id => nil} + + get :show, :id => 11 + + assert_response :success + assert_equal query, assigns(:query) + # Previous and next issues for all projects + assert_equal 8, assigns(:prev_issue_id) + assert_equal 12, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/8]', :text => /Previous/ + assert_select 'a[href=/issues/12]', :text => /Next/ + end + end + + def test_show_should_display_prev_next_links_with_query_and_sort_on_association + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil} + + %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort| + @request.session['issues_index_sort'] = assoc_sort + + get :show, :id => 3 + assert_response :success, "Wrong response status for #{assoc_sort} sort" + + assert_select 'div.next-prev-links' do + assert_select 'a', :text => /(Previous|Next)/ + end + end + end + + def test_show_should_display_prev_next_links_with_project_query_in_session + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1} + @request.session['issues_index_sort'] = 'id' + + with_settings :display_subprojects_issues => '0' do + get :show, :id => 3 + end + + assert_response :success + # Previous and next issues inside project + assert_equal 2, assigns(:prev_issue_id) + assert_equal 7, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/2]', :text => /Previous/ + assert_select 'a[href=/issues/7]', :text => /Next/ + end + end + + def test_show_should_not_display_prev_link_for_first_issue + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1} + @request.session['issues_index_sort'] = 'id' + + with_settings :display_subprojects_issues => '0' do + get :show, :id => 1 + end + + assert_response :success + assert_nil assigns(:prev_issue_id) + assert_equal 2, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a', :text => /Previous/, :count => 0 + assert_select 'a[href=/issues/2]', :text => /Next/ + end + end + + def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results + @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1} + @request.session['issues_index_sort'] = 'id' + + get :show, :id => 1 + + assert_response :success + assert_nil assigns(:prev_issue_id) + assert_nil assigns(:next_issue_id) + + assert_select 'a', :text => /Previous/, :count => 0 + assert_select 'a', :text => /Next/, :count => 0 + end + + def test_show_show_should_display_prev_next_links_with_query_sort_by_user_custom_field + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') + CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') + + query = IssueQuery.create!(:name => 'test', :visibility => IssueQuery::VISIBILITY_PUBLIC, :user_id => 1, :filters => {}, + :sort_criteria => [["cf_#{cf.id}", 'asc'], ['id', 'asc']]) + @request.session[:query] = {:id => query.id, :project_id => nil} + + get :show, :id => 3 + assert_response :success + + assert_equal 2, assigns(:prev_issue_id) + assert_equal 1, assigns(:next_issue_id) + + assert_select 'div.next-prev-links' do + assert_select 'a[href=/issues/2]', :text => /Previous/ + assert_select 'a[href=/issues/1]', :text => /Next/ + end + end + + def test_show_should_display_link_to_the_assignee + get :show, :id => 2 + assert_response :success + assert_select '.assigned-to' do + assert_select 'a[href=/users/3]' + end + end + + def test_show_should_display_visible_changesets_from_other_projects + project = Project.find(2) + issue = project.issues.first + issue.changeset_ids = [102] + issue.save! + # changesets from other projects should be displayed even if repository + # is disabled on issue's project + project.disable_module! :repository + + @request.session[:user_id] = 2 + get :show, :id => issue.id + + assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/3' + end + + def test_show_should_display_watchers + @request.session[:user_id] = 2 + Issue.find(1).add_watcher User.find(2) + + get :show, :id => 1 + assert_select 'div#watchers ul' do + assert_select 'li' do + assert_select 'a[href=/users/2]' + assert_select 'a img[alt=Delete]' + end + end + end + + def test_show_should_display_watchers_with_gravatars + @request.session[:user_id] = 2 + Issue.find(1).add_watcher User.find(2) + + with_settings :gravatar_enabled => '1' do + get :show, :id => 1 + end + + assert_select 'div#watchers ul' do + assert_select 'li' do + assert_select 'img.gravatar' + assert_select 'a[href=/users/2]' + assert_select 'a img[alt=Delete]' + end + end + end + + def test_show_with_thumbnails_enabled_should_display_thumbnails + @request.session[:user_id] = 2 + + with_settings :thumbnails_enabled => '1' do + get :show, :id => 14 + assert_response :success + end + + assert_select 'div.thumbnails' do + assert_select 'a[href=/attachments/16/testfile.png]' do + assert_select 'img[src=/attachments/thumbnail/16]' + end + end + end + + def test_show_with_thumbnails_disabled_should_not_display_thumbnails + @request.session[:user_id] = 2 + + with_settings :thumbnails_enabled => '0' do + get :show, :id => 14 + assert_response :success + end + + assert_select 'div.thumbnails', 0 + end + + def test_show_with_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + get :show, :id => 1 + assert_response :success + + assert_select 'td', :text => 'MySQL, Oracle' + end + + def test_show_with_multi_user_custom_field + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + issue = Issue.find(1) + issue.custom_field_values = {field.id => ['2', '3']} + issue.save! + + get :show, :id => 1 + assert_response :success + + assert_select "td.cf_#{field.id}", :text => 'Dave Lopper, John Smith' do + assert_select 'a', :text => 'Dave Lopper' + assert_select 'a', :text => 'John Smith' + end + end + + def test_show_should_display_private_notes_with_permission_only + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) + @request.session[:user_id] = 2 + + get :show, :id => 2 + assert_response :success + assert_include journal, assigns(:journals) + + Role.find(1).remove_permission! :view_private_notes + get :show, :id => 2 + assert_response :success + assert_not_include journal, assigns(:journals) + end + + def test_show_atom + get :show, :id => 2, :format => 'atom' + assert_response :success + assert_template 'journals/index' + # Inline image + assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10')) + end + + def test_show_export_to_pdf + get :show, :id => 3, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:issue) + end + + def test_show_export_to_pdf_with_ancestors + issue = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1) + + get :show, :id => issue.id, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_export_to_pdf_with_descendants + c1 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1) + c2 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1) + c3 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => c1.id) + + get :show, :id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_export_to_pdf_with_journals + get :show, :id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + + def test_show_export_to_pdf_with_changesets + [[100], [100, 101], [100, 101, 102]].each do |cs| + issue1 = Issue.find(3) + issue1.changesets = Changeset.find(cs) + issue1.save! + issue = Issue.find(3) + assert_equal issue.changesets.count, cs.size + get :show, :id => 3, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + end + end + + def test_show_invalid_should_respond_with_404 + get :show, :id => 999 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]' + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]' + assert_select 'input[name=?]', 'issue[subject]' + assert_select 'textarea[name=?]', 'issue[description]' + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]' + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]' + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]' + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]' + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string' + assert_select 'input[name=?]', 'issue[watcher_user_ids][]' + end + + # Be sure we don't display inactive IssuePriorities + assert ! IssuePriority.find(15).active? + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end + end + + def test_get_new_with_minimal_permissions + Role.find(1).update_attribute :permissions, [:add_issues] + WorkflowTransition.delete_all :role_id => 1 + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'form#issue-form' do + assert_select 'input[name=?]', 'issue[is_private]', 0 + assert_select 'select[name=?]', 'issue[project_id]', 0 + assert_select 'select[name=?]', 'issue[tracker_id]' + assert_select 'input[name=?]', 'issue[subject]' + assert_select 'textarea[name=?]', 'issue[description]' + assert_select 'select[name=?]', 'issue[status_id]' + assert_select 'select[name=?]', 'issue[priority_id]' + assert_select 'select[name=?]', 'issue[assigned_to_id]' + assert_select 'select[name=?]', 'issue[category_id]' + assert_select 'select[name=?]', 'issue[fixed_version_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]' + assert_select 'select[name=?]', 'issue[done_ratio]' + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string' + assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 + end + end + + def test_get_new_with_list_custom_field + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select.list_cf[name=?]', 'issue[custom_field_values][1]' do + assert_select 'option', 4 + assert_select 'option[value=MySQL]', :text => 'MySQL' + end + end + + def test_get_new_with_multi_custom_field + field = IssueCustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do + assert_select 'option', 3 + assert_select 'option[value=MySQL]', :text => 'MySQL' + end + assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', '' + end + + def test_get_new_with_multi_user_custom_field + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?][multiple=multiple]', "issue[custom_field_values][#{field.id}][]" do + assert_select 'option', Project.find(1).users.count + assert_select 'option[value=2]', :text => 'John Smith' + end + assert_select 'input[name=?][type=hidden][value=?]', "issue[custom_field_values][#{field.id}][]", '' + end + + def test_get_new_with_date_custom_field + field = IssueCustomField.create!(:name => 'Date', :field_format => 'date', :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + + assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]" + end + + def test_get_new_with_text_custom_field + field = IssueCustomField.create!(:name => 'Text', :field_format => 'text', :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + + assert_select 'textarea[name=?]', "issue[custom_field_values][#{field.id}]" + end + + def test_get_new_without_default_start_date_is_creation_date + with_settings :default_issue_start_date_to_creation_date => 0 do + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?][value]', 'issue[start_date]', 0 + end + end + + def test_get_new_with_default_start_date_is_creation_date + with_settings :default_issue_start_date_to_creation_date => 1 do + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + assert_select 'input[name=?][value=?]', 'issue[start_date]', + Date.today.to_s + end + end + + def test_get_new_form_should_allow_attachment_upload + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + + assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do + assert_select 'input[name=?][type=file]', 'attachments[dummy][file]' + end + end + + def test_get_new_should_prefill_the_form_from_params + @request.session[:user_id] = 2 + get :new, :project_id => 1, + :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}} + + issue = assigns(:issue) + assert_equal 3, issue.tracker_id + assert_equal 'Prefilled', issue.description + assert_equal 'Custom field value', issue.custom_field_value(2) + + assert_select 'select[name=?]', 'issue[tracker_id]' do + assert_select 'option[value=3][selected=selected]' + end + assert_select 'textarea[name=?]', 'issue[description]', :text => /Prefilled/ + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Custom field value' + end + + def test_get_new_should_mark_required_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'label[for=issue_start_date]' do + assert_select 'span[class=required]', 0 + end + assert_select 'label[for=issue_due_date]' do + assert_select 'span[class=required]' + end + assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do + assert_select 'span[class=required]', 0 + end + assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do + assert_select 'span[class=required]' + end + end + + def test_get_new_should_not_display_readonly_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'input[name=?]', 'issue[start_date]' + assert_select 'input[name=?]', 'issue[due_date]', 0 + assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]" + assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0 + end + + def test_get_new_without_tracker_id + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + issue = assigns(:issue) + assert_not_nil issue + assert_equal Project.find(1).trackers.first, issue.tracker + end + + def test_get_new_with_no_default_status_should_display_an_error + @request.session[:user_id] = 2 + IssueStatus.delete_all + + get :new, :project_id => 1 + assert_response 500 + assert_error_tag :content => /No default issue/ + end + + def test_get_new_with_no_tracker_should_display_an_error + @request.session[:user_id] = 2 + Tracker.delete_all + + get :new, :project_id => 1 + assert_response 500 + assert_error_tag :content => /No tracker/ + end + + def test_update_form_for_new_issue + @request.session[:user_id] = 2 + xhr :post, :update_form, :project_id => 1, + :issue => {:tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_template 'update_form' + assert_template :partial => '_form' + assert_equal 'text/javascript', response.content_type + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 'This is the test_new issue', issue.subject + end + + def test_update_form_for_new_issue_should_propose_transitions_based_on_initial_status + @request.session[:user_id] = 2 + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4) + + xhr :post, :update_form, :project_id => 1, + :issue => {:tracker_id => 1, + :status_id => 5, + :subject => 'This is an issue'} + + assert_equal 5, assigns(:issue).status_id + assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort + end + + def test_post_create + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :status_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :start_date => '2010-11-07', + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_equal 2, issue.author_id + assert_equal 3, issue.tracker_id + assert_equal 2, issue.status_id + assert_equal Date.parse('2010-11-07'), issue.start_date + assert_nil issue.estimated_hours + v = issue.custom_values.where(:custom_field_id => 2).first + assert_not_nil v + assert_equal 'Value for field 2', v.value + end + + def test_post_new_with_group_assignment + group = Group.find(11) + project = Project.find(1) + project.members << Member.new(:principal => group, :roles => [Role.givable.first]) + + with_settings :issue_group_assignment => '1' do + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => project.id, + :issue => {:tracker_id => 3, + :status_id => 1, + :subject => 'This is the test_new_with_group_assignment issue', + :assigned_to_id => group.id} + end + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue') + assert_not_nil issue + assert_equal group, issue.assigned_to + end + + def test_post_create_without_start_date_and_default_start_date_is_not_creation_date + with_settings :default_issue_start_date_to_creation_date => 0 do + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :status_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', + :id => Issue.last.id + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_nil issue.start_date + end + end + + def test_post_create_without_start_date_and_default_start_date_is_creation_date + with_settings :default_issue_start_date_to_creation_date => 1 do + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :status_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', + :id => Issue.last.id + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_equal Date.today, issue.start_date + end + end + + def test_post_create_and_continue + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5}, + :continue => '' + end + + issue = Issue.order('id DESC').first + assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3} + assert_not_nil flash[:notice], "flash was not set" + assert_include %|##{issue.id}|, flash[:notice], "issue link not found in the flash message" + end + + def test_post_create_without_custom_fields_param + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + end + + def test_post_create_with_multi_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:multiple, true) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}} + end + assert_response 302 + issue = Issue.order('id DESC').first + assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort + end + + def test_post_create_with_empty_multi_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:multiple, true) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :custom_field_values => {'1' => ['']}} + end + assert_response 302 + issue = Issue.order('id DESC').first + assert_equal [''], issue.custom_field_value(1).sort + end + + def test_post_create_with_multi_user_custom_field + field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true, + :tracker_ids => [1], :is_for_all => true) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :custom_field_values => {field.id.to_s => ['', '2', '3']}} + end + assert_response 302 + issue = Issue.order('id DESC').first + assert_equal ['2', '3'], issue.custom_field_value(field).sort + end + + def test_post_create_with_required_custom_field_and_without_custom_fields_param + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + @request.session[:user_id] = 2 + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + end + assert_response :success + assert_template 'new' + issue = assigns(:issue) + assert_not_nil issue + assert_error_tag :content => /Database #{ESCAPED_CANT} be blank/ + end + + def test_create_should_validate_required_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, :issue => { + :tracker_id => 2, + :status_id => 1, + :subject => 'Test', + :start_date => '', + :due_date => '', + :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''} + } + assert_response :success + assert_template 'new' + end + + assert_error_tag :content => /Due date #{ESCAPED_CANT} be blank/i + assert_error_tag :content => /Bar #{ESCAPED_CANT} be blank/i + end + + def test_create_should_ignore_readonly_fields + cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly') + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, :issue => { + :tracker_id => 2, + :status_id => 1, + :subject => 'Test', + :start_date => '2012-07-14', + :due_date => '2012-07-16', + :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'} + } + assert_response 302 + end + + issue = Issue.order('id DESC').first + assert_equal Date.parse('2012-07-14'), issue.start_date + assert_nil issue.due_date + assert_equal 'value1', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + end + + def test_post_create_with_watchers + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + assert_difference 'Watcher.count', 2 do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a new issue with watchers', + :description => 'This is the description', + :priority_id => 5, + :watcher_user_ids => ['2', '3']} + end + issue = Issue.find_by_subject('This is a new issue with watchers') + assert_not_nil issue + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + + # Watchers added + assert_equal [2, 3], issue.watcher_user_ids.sort + assert issue.watched_by?(User.find(3)) + # Watchers notified + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail) + end + + def test_post_create_subissue + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '2'} + assert_response 302 + end + issue = Issue.order('id DESC').first + assert_equal Issue.find(2), issue.parent + end + + def test_post_create_subissue_with_sharp_parent_id + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '#2'} + assert_response 302 + end + issue = Issue.order('id DESC').first + assert_equal Issue.find(2), issue.parent + end + + def test_post_create_subissue_with_non_visible_parent_id_should_not_validate + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '4'} + + assert_response :success + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '4' + assert_error_tag :content => /Parent task is invalid/i + end + end + + def test_post_create_subissue_with_non_numeric_parent_id_should_not_validate + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a child issue', + :parent_issue_id => '01ABC'} + + assert_response :success + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '01ABC' + assert_error_tag :content => /Parent task is invalid/i + end + end + + def test_post_create_private + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a private issue', + :is_private => '1'} + end + issue = Issue.order('id DESC').first + assert issue.is_private? + end + + def test_post_create_private_with_set_own_issues_private_permission + role = Role.find(1) + role.remove_permission! :set_issues_private + role.add_permission! :set_own_issues_private + + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is a private issue', + :is_private => '1'} + end + issue = Issue.order('id DESC').first + assert issue.is_private? + end + + def test_post_create_should_send_a_notification + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 3, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_post_create_should_preserve_fields_values_on_validation_failure + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + # empty subject + :subject => '', + :description => 'This is a description', + :priority_id => 6, + :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}} + assert_response :success + assert_template 'new' + + assert_select 'textarea[name=?]', 'issue[description]', :text => 'This is a description' + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=6][selected=selected]', :text => 'High' + end + # Custom fields + assert_select 'select[name=?]', 'issue[custom_field_values][1]' do + assert_select 'option[value=Oracle][selected=selected]', :text => 'Oracle' + end + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Value for field 2' + end + + def test_post_create_with_failure_should_preserve_watchers + assert !User.find(8).member_of?(Project.find(1)) + + @request.session[:user_id] = 2 + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :watcher_user_ids => ['3', '8']} + assert_response :success + assert_template 'new' + + assert_select 'input[name=?][value=2]:not(checked)', 'issue[watcher_user_ids][]' + assert_select 'input[name=?][value=3][checked=checked]', 'issue[watcher_user_ids][]' + assert_select 'input[name=?][value=8][checked=checked]', 'issue[watcher_user_ids][]' + end + + def test_post_create_should_ignore_non_safe_attributes + @request.session[:user_id] = 2 + assert_nothing_raised do + post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" } + end + end + + def test_post_create_with_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + assert_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => 'With attachment' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + end + + issue = Issue.order('id DESC').first + attachment = Attachment.order('id DESC').first + + assert_equal issue, attachment.container + assert_equal 2, attachment.author_id + assert_equal 'testfile.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test file', attachment.description + assert_equal 59, attachment.filesize + assert File.exists?(attachment.diskfile) + assert_equal 59, File.size(attachment.diskfile) + end + + def test_post_create_with_attachment_should_notify_with_attachments + ActionMailer::Base.deliveries.clear + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + with_settings :host_name => 'mydomain.foo', :protocol => 'http' do + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => 'With attachment' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + end + + assert_not_nil ActionMailer::Base.deliveries.last + assert_select_email do + assert_select 'a[href^=?]', 'http://mydomain.foo/attachments/download', 'testfile.txt' + end + end + + def test_post_create_with_failure_should_save_attachments + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + assert_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => '' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + assert_response :success + assert_template 'new' + end + end + + attachment = Attachment.order('id DESC').first + assert_equal 'testfile.txt', attachment.filename + assert File.exists?(attachment.diskfile) + assert_nil attachment.container + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_post_create_with_failure_should_keep_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + assert_no_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => '' }, + :attachments => {'p0' => {'token' => attachment.token}} + assert_response :success + assert_template 'new' + end + end + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_post_create_should_attach_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_difference 'Issue.count' do + assert_no_difference 'Attachment.count' do + post :create, :project_id => 1, + :issue => { :tracker_id => '1', :subject => 'Saved attachments' }, + :attachments => {'p0' => {'token' => attachment.token}} + assert_response 302 + end + end + + issue = Issue.order('id DESC').first + assert_equal 1, issue.attachments.count + + attachment.reload + assert_equal issue, attachment.container + end + + def setup_without_workflow_privilege + WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id]) + Role.anonymous.add_permission! :add_issues, :add_issue_notes + end + private :setup_without_workflow_privilege + + test "without workflow privilege #new should propose default status only" do + setup_without_workflow_privilege + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option', 1 + assert_select 'option[value=?]', IssueStatus.default.id.to_s + end + end + + test "without workflow privilege #new should accept default status" do + setup_without_workflow_privilege + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is an issue', + :status_id => 1} + end + issue = Issue.order('id').last + assert_equal IssueStatus.default, issue.status + end + + test "without workflow privilege #new should ignore unauthorized status" do + setup_without_workflow_privilege + assert_difference 'Issue.count' do + post :create, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is an issue', + :status_id => 3} + end + issue = Issue.order('id').last + assert_equal IssueStatus.default, issue.status + end + + test "without workflow privilege #update should ignore status change" do + setup_without_workflow_privilege + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'} + end + assert_equal 1, Issue.find(1).status_id + end + + test "without workflow privilege #update ignore attributes changes" do + setup_without_workflow_privilege + assert_difference 'Journal.count' do + put :update, :id => 1, + :issue => {:subject => 'changed', :assigned_to_id => 2, + :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal "Can't print recipes", issue.subject + assert_nil issue.assigned_to + end + + def setup_with_workflow_privilege + WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id]) + WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 3) + WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 4) + Role.anonymous.add_permission! :add_issues, :add_issue_notes + end + private :setup_with_workflow_privilege + + test "with workflow privilege #update should accept authorized status" do + setup_with_workflow_privilege + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'} + end + assert_equal 3, Issue.find(1).status_id + end + + test "with workflow privilege #update should ignore unauthorized status" do + setup_with_workflow_privilege + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'} + end + assert_equal 1, Issue.find(1).status_id + end + + test "with workflow privilege #update should accept authorized attributes changes" do + setup_with_workflow_privilege + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:assigned_to_id => 2, :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal 2, issue.assigned_to_id + end + + test "with workflow privilege #update should ignore unauthorized attributes changes" do + setup_with_workflow_privilege + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:subject => 'changed', :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal "Can't print recipes", issue.subject + end + + def setup_with_workflow_privilege_and_edit_issues_permission + setup_with_workflow_privilege + Role.anonymous.add_permission! :add_issues, :edit_issues + end + private :setup_with_workflow_privilege_and_edit_issues_permission + + test "with workflow privilege and :edit_issues permission should accept authorized status" do + setup_with_workflow_privilege_and_edit_issues_permission + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'} + end + assert_equal 3, Issue.find(1).status_id + end + + test "with workflow privilege and :edit_issues permission should ignore unauthorized status" do + setup_with_workflow_privilege_and_edit_issues_permission + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'} + end + assert_equal 1, Issue.find(1).status_id + end + + test "with workflow privilege and :edit_issues permission should accept authorized attributes changes" do + setup_with_workflow_privilege_and_edit_issues_permission + assert_difference 'Journal.count' do + put :update, :id => 1, + :issue => {:subject => 'changed', :assigned_to_id => 2, + :notes => 'just trying'} + end + issue = Issue.find(1) + assert_equal "changed", issue.subject + assert_equal 2, issue.assigned_to_id + end + + def test_new_as_copy + @request.session[:user_id] = 2 + get :new, :project_id => 1, :copy_from => 1 + + assert_response :success + assert_template 'new' + + assert_not_nil assigns(:issue) + orig = Issue.find(1) + assert_equal 1, assigns(:issue).project_id + assert_equal orig.subject, assigns(:issue).subject + assert assigns(:issue).copy? + + assert_select 'form[id=issue-form][action=/projects/ecookbook/issues]' do + assert_select 'select[name=?]', 'issue[project_id]' do + assert_select 'option[value=1][selected=selected]', :text => 'eCookbook' + assert_select 'option[value=2]:not([selected])', :text => 'OnlineStore' + end + assert_select 'input[name=copy_from][value=1]' + end + + # "New issue" menu item should not link to copy + assert_select '#main-menu a.new-issue[href=/projects/ecookbook/issues/new]' + end + + def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox + @request.session[:user_id] = 2 + issue = Issue.find(3) + assert issue.attachments.count > 0 + get :new, :project_id => 1, :copy_from => 3 + + assert_select 'input[name=copy_attachments][type=checkbox][checked=checked][value=1]' + end + + def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox + @request.session[:user_id] = 2 + issue = Issue.find(3) + issue.attachments.delete_all + get :new, :project_id => 1, :copy_from => 3 + + assert_select 'input[name=copy_attachments]', 0 + end + + def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox + @request.session[:user_id] = 2 + issue = Issue.generate_with_descendants! + get :new, :project_id => 1, :copy_from => issue.id + + assert_select 'input[type=checkbox][name=copy_subtasks][checked=checked][value=1]' + end + + def test_new_as_copy_with_invalid_issue_should_respond_with_404 + @request.session[:user_id] = 2 + get :new, :project_id => 1, :copy_from => 99999 + assert_response 404 + end + + def test_create_as_copy_on_different_project + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'} + + assert_not_nil assigns(:issue) + assert assigns(:issue).copy? + end + issue = Issue.order('id DESC').first + assert_redirected_to "/issues/#{issue.id}" + + assert_equal 2, issue.project_id + assert_equal 3, issue.tracker_id + assert_equal 'Copy', issue.subject + end + + def test_create_as_copy_should_copy_attachments + @request.session[:user_id] = 2 + issue = Issue.find(3) + count = issue.attachments.count + assert count > 0 + assert_difference 'Issue.count' do + assert_difference 'Attachment.count', count do + assert_difference 'Journal.count', 2 do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with attachments'}, + :copy_attachments => '1' + end + end + end + copy = Issue.order('id DESC').first + assert_equal count, copy.attachments.count + assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort + end + + def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments + @request.session[:user_id] = 2 + issue = Issue.find(3) + count = issue.attachments.count + assert count > 0 + assert_difference 'Issue.count' do + assert_no_difference 'Attachment.count' do + assert_difference 'Journal.count', 2 do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with attachments'} + end + end + end + copy = Issue.order('id DESC').first + assert_equal 0, copy.attachments.count + end + + def test_create_as_copy_with_attachments_should_add_new_files + @request.session[:user_id] = 2 + issue = Issue.find(3) + count = issue.attachments.count + assert count > 0 + assert_difference 'Issue.count' do + assert_difference 'Attachment.count', count + 1 do + assert_difference 'Journal.count', 2 do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with attachments'}, + :copy_attachments => '1', + :attachments => {'1' => + {'file' => uploaded_test_file('testfile.txt', 'text/plain'), + 'description' => 'test file'}} + end + end + end + copy = Issue.order('id DESC').first + assert_equal count + 1, copy.attachments.count + end + + def test_create_as_copy_should_add_relation_with_copied_issue + @request.session[:user_id] = 2 + assert_difference 'Issue.count' do + assert_difference 'IssueRelation.count' do + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy'} + end + end + copy = Issue.order('id DESC').first + assert_equal 1, copy.relations.size + end + + def test_create_as_copy_should_copy_subtasks + @request.session[:user_id] = 2 + issue = Issue.generate_with_descendants! + count = issue.descendants.count + assert_difference 'Issue.count', count + 1 do + assert_difference 'Journal.count', (count + 1) * 2 do + post :create, :project_id => 1, :copy_from => issue.id, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with subtasks'}, + :copy_subtasks => '1' + end + end + copy = Issue.where(:parent_id => nil).order('id DESC').first + assert_equal count, copy.descendants.count + assert_equal issue.descendants.map(&:subject).sort, copy.descendants.map(&:subject).sort + end + + def test_create_as_copy_without_copy_subtasks_option_should_not_copy_subtasks + @request.session[:user_id] = 2 + issue = Issue.generate_with_descendants! + assert_difference 'Issue.count', 1 do + assert_difference 'Journal.count', 2 do + post :create, :project_id => 1, :copy_from => 3, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with subtasks'} + end + end + copy = Issue.where(:parent_id => nil).order('id DESC').first + assert_equal 0, copy.descendants.count + end + + def test_create_as_copy_with_failure + @request.session[:user_id] = 2 + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''} + + assert_response :success + assert_template 'new' + + assert_not_nil assigns(:issue) + assert assigns(:issue).copy? + + assert_select 'form#issue-form[action=/projects/ecookbook/issues]' do + assert_select 'select[name=?]', 'issue[project_id]' do + assert_select 'option[value=1]:not([selected])', :text => 'eCookbook' + assert_select 'option[value=2][selected=selected]', :text => 'OnlineStore' + end + assert_select 'input[name=copy_from][value=1]' + end + end + + def test_create_as_copy_on_project_without_permission_should_ignore_target_project + @request.session[:user_id] = 2 + assert !User.find(2).member_of?(Project.find(4)) + + assert_difference 'Issue.count' do + post :create, :project_id => 1, :copy_from => 1, + :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'} + end + issue = Issue.order('id DESC').first + assert_equal 1, issue.project_id + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_not_nil assigns(:issue) + assert_equal Issue.find(1), assigns(:issue) + + # Be sure we don't display inactive IssuePriorities + assert ! IssuePriority.find(15).active? + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end + end + + def test_get_edit_should_display_the_time_entry_form_with_log_time_permission + @request.session[:user_id] = 2 + Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time] + + get :edit, :id => 1 + assert_select 'input[name=?]', 'time_entry[hours]' + end + + def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :log_time + + get :edit, :id => 1 + assert_select 'input[name=?]', 'time_entry[hours]', 0 + end + + def test_get_edit_with_params + @request.session[:user_id] = 2 + get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }, + :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => 10 } + assert_response :success + assert_template 'edit' + + issue = assigns(:issue) + assert_not_nil issue + + assert_equal 5, issue.status_id + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option[value=5][selected=selected]', :text => 'Closed' + end + + assert_equal 7, issue.priority_id + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=7][selected=selected]', :text => 'Urgent' + end + + assert_select 'input[name=?][value=2.5]', 'time_entry[hours]' + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[value=10][selected=selected]', :text => 'Development' + end + assert_select 'input[name=?][value=test_get_edit_with_params]', 'time_entry[comments]' + end + + def test_get_edit_with_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + + assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do + assert_select 'option', 3 + assert_select 'option[value=MySQL][selected=selected]' + assert_select 'option[value=Oracle][selected=selected]' + assert_select 'option[value=PostgreSQL]:not([selected])' + end + end + + def test_update_form_for_existing_issue + @request.session[:user_id] = 2 + xhr :put, :update_form, :project_id => 1, + :id => 1, + :issue => {:tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_equal 'text/javascript', response.content_type + assert_template 'update_form' + assert_template :partial => '_form' + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 1, issue.id + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 'This is the test_new issue', issue.subject + end + + def test_update_form_for_existing_issue_should_keep_issue_author + @request.session[:user_id] = 3 + xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'} + assert_response :success + assert_equal 'text/javascript', response.content_type + + issue = assigns(:issue) + assert_equal User.find(2), issue.author + assert_equal 2, issue.author_id + assert_not_equal User.current, issue.author + end + + def test_update_form_for_existing_issue_should_propose_transitions_based_on_initial_status + @request.session[:user_id] = 2 + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4) + + xhr :put, :update_form, :project_id => 1, + :id => 2, + :issue => {:tracker_id => 2, + :status_id => 5, + :subject => 'This is an issue'} + + assert_equal 5, assigns(:issue).status_id + assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort + end + + def test_update_form_for_existing_issue_with_project_change + @request.session[:user_id] = 2 + xhr :put, :update_form, :project_id => 1, + :id => 1, + :issue => {:project_id => 2, + :tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_template :partial => '_form' + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 1, issue.id + assert_equal 2, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 'This is the test_new issue', issue.subject + end + + def test_update_form_should_propose_default_status_for_existing_issue + @request.session[:user_id] = 2 + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3) + + xhr :put, :update_form, :project_id => 1, :id => 2 + assert_response :success + assert_equal [2,3], assigns(:allowed_statuses).map(&:id).sort + end + + def test_put_update_without_custom_fields_param + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + issue = Issue.find(1) + assert_equal '125', issue.custom_value_for(2).value + old_subject = issue.subject + new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 2) do + put :update, :id => 1, :issue => {:subject => new_subject, + :priority_id => '6', + :category_id => '1' # no change + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue.reload + assert_equal new_subject, issue.subject + # Make sure custom fields were not cleared + assert_equal '125', issue.custom_value_for(2).value + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") + assert_mail_body_match "Subject changed from #{old_subject} to #{new_subject}", mail + end + + def test_put_update_with_project_change + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + put :update, :id => 1, :issue => {:project_id => '2', + :tracker_id => '1', # no change + :priority_id => '6', + :category_id => '3' + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue = Issue.find(1) + assert_equal 2, issue.project_id + assert_equal 1, issue.tracker_id + assert_equal 6, issue.priority_id + assert_equal 3, issue.category_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") + assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail + end + + def test_put_update_with_tracker_change + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 2) do + put :update, :id => 1, :issue => {:project_id => '1', + :tracker_id => '2', + :priority_id => '6' + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue = Issue.find(1) + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 6, issue.priority_id + assert_equal 1, issue.category_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") + assert_mail_body_match "Tracker changed from Bug to Feature request", mail + end + + def test_put_update_with_custom_field_change + @request.session[:user_id] = 2 + issue = Issue.find(1) + assert_equal '125', issue.custom_value_for(2).value + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + put :update, :id => 1, :issue => {:subject => 'Custom field change', + :priority_id => '6', + :category_id => '1', # no change + :custom_field_values => { '2' => 'New custom value' } + } + end + end + assert_redirected_to :action => 'show', :id => '1' + issue.reload + assert_equal 'New custom value', issue.custom_value_for(2).value + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_mail_body_match "Searchable field changed from 125 to New custom value", mail + end + + def test_put_update_with_multi_custom_field_change + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(1) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + + @request.session[:user_id] = 2 + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + put :update, :id => 1, + :issue => { + :subject => 'Custom field change', + :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] } + } + end + end + assert_redirected_to :action => 'show', :id => '1' + assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort + end + + def test_put_update_with_status_and_assignee_change + issue = Issue.find(1) + assert_equal 1, issue.status_id + @request.session[:user_id] = 2 + assert_difference('TimeEntry.count', 0) do + put :update, + :id => 1, + :issue => { :status_id => 2, :assigned_to_id => 3, :notes => 'Assigned to dlopper' }, + :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first } + end + assert_redirected_to :action => 'show', :id => '1' + issue.reload + assert_equal 2, issue.status_id + j = Journal.order('id DESC').first + assert_equal 'Assigned to dlopper', j.notes + assert_equal 2, j.details.size + + mail = ActionMailer::Base.deliveries.last + assert_mail_body_match "Status changed from New to Assigned", mail + # subject should contain the new status + assert mail.subject.include?("(#{ IssueStatus.find(2).name })") + end + + def test_put_update_with_note_only + notes = 'Note added by IssuesControllerTest#test_update_with_note_only' + # anonymous user + put :update, + :id => 1, + :issue => { :notes => notes } + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_equal notes, j.notes + assert_equal 0, j.details.size + assert_equal User.anonymous, j.user + + mail = ActionMailer::Base.deliveries.last + assert_mail_body_match notes, mail + end + + def test_put_update_with_private_note_only + notes = 'Private note' + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + put :update, :id => 1, :issue => {:notes => notes, :private_notes => '1'} + assert_redirected_to :action => 'show', :id => '1' + end + + j = Journal.order('id DESC').first + assert_equal notes, j.notes + assert_equal true, j.private_notes + end + + def test_put_update_with_private_note_and_changes + notes = 'Private note' + @request.session[:user_id] = 2 + + assert_difference 'Journal.count', 2 do + put :update, :id => 1, :issue => {:subject => 'New subject', :notes => notes, :private_notes => '1'} + assert_redirected_to :action => 'show', :id => '1' + end + + j = Journal.order('id DESC').first + assert_equal notes, j.notes + assert_equal true, j.private_notes + assert_equal 0, j.details.count + + j = Journal.order('id DESC').offset(1).first + assert_nil j.notes + assert_equal false, j.private_notes + assert_equal 1, j.details.count + end + + def test_put_update_with_note_and_spent_time + @request.session[:user_id] = 2 + spent_hours_before = Issue.find(1).spent_hours + assert_difference('TimeEntry.count') do + put :update, + :id => 1, + :issue => { :notes => '2.5 hours added' }, + :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id } + end + assert_redirected_to :action => 'show', :id => '1' + + issue = Issue.find(1) + + j = Journal.order('id DESC').first + assert_equal '2.5 hours added', j.notes + assert_equal 0, j.details.size + + t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time') + assert_not_nil t + assert_equal 2.5, t.hours + assert_equal spent_hours_before + 2.5, issue.spent_hours + end + + def test_put_update_should_preserve_parent_issue_even_if_not_visible + parent = Issue.generate!(:project_id => 1, :is_private => true) + issue = Issue.generate!(:parent_issue_id => parent.id) + assert !parent.visible?(User.find(3)) + @request.session[:user_id] = 3 + + get :edit, :id => issue.id + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', parent.id.to_s + + put :update, :id => issue.id, :issue => {:subject => 'New subject', :parent_issue_id => parent.id.to_s} + assert_response 302 + assert_equal parent, issue.parent + end + + def test_put_update_with_attachment_only + set_tmp_attachments_directory + + # Delete all fixtured journals, a race condition can occur causing the wrong + # journal to get fetched in the next find. + Journal.delete_all + + # anonymous user + assert_difference 'Attachment.count' do + put :update, :id => 1, + :issue => {:notes => ''}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + + assert_redirected_to :action => 'show', :id => '1' + j = Issue.find(1).journals.reorder('id DESC').first + assert j.notes.blank? + assert_equal 1, j.details.size + assert_equal 'testfile.txt', j.details.first.value + assert_equal User.anonymous, j.user + + attachment = Attachment.order('id DESC').first + assert_equal Issue.find(1), attachment.container + assert_equal User.anonymous, attachment.author + assert_equal 'testfile.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test file', attachment.description + assert_equal 59, attachment.filesize + assert File.exists?(attachment.diskfile) + assert_equal 59, File.size(attachment.diskfile) + + mail = ActionMailer::Base.deliveries.last + assert_mail_body_match 'testfile.txt', mail + end + + def test_put_update_with_failure_should_save_attachments + set_tmp_attachments_directory + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_difference 'Attachment.count' do + put :update, :id => 1, + :issue => { :subject => '' }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + assert_response :success + assert_template 'edit' + end + end + + attachment = Attachment.order('id DESC').first + assert_equal 'testfile.txt', attachment.filename + assert File.exists?(attachment.diskfile) + assert_nil attachment.container + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_put_update_with_failure_should_keep_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_no_difference 'Attachment.count' do + put :update, :id => 1, + :issue => { :subject => '' }, + :attachments => {'p0' => {'token' => attachment.token}} + assert_response :success + assert_template 'edit' + end + end + + assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt' + end + + def test_put_update_should_attach_saved_attachments + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + assert_no_difference 'Attachment.count' do + put :update, :id => 1, + :issue => {:notes => 'Attachment added'}, + :attachments => {'p0' => {'token' => attachment.token}} + assert_redirected_to '/issues/1' + end + end + end + + attachment.reload + assert_equal Issue.find(1), attachment.container + + journal = Journal.order('id DESC').first + assert_equal 1, journal.details.size + assert_equal 'testfile.txt', journal.details.first.value + end + + def test_put_update_with_attachment_that_fails_to_save + set_tmp_attachments_directory + + # Delete all fixtured journals, a race condition can occur causing the wrong + # journal to get fetched in the next find. + Journal.delete_all + + # Mock out the unsaved attachment + Attachment.any_instance.stubs(:create).returns(Attachment.new) + + # anonymous user + put :update, + :id => 1, + :issue => {:notes => ''}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + assert_redirected_to :action => 'show', :id => '1' + assert_equal '1 file(s) could not be saved.', flash[:warning] + end + + def test_put_update_with_no_change + issue = Issue.find(1) + issue.journals.clear + ActionMailer::Base.deliveries.clear + + put :update, + :id => 1, + :issue => {:notes => ''} + assert_redirected_to :action => 'show', :id => '1' + + issue.reload + assert issue.journals.empty? + # No email should be sent + assert ActionMailer::Base.deliveries.empty? + end + + def test_put_update_should_send_a_notification + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + issue = Issue.find(1) + old_subject = issue.subject + new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' + + put :update, :id => 1, :issue => {:subject => new_subject, + :priority_id => '6', + :category_id => '1' # no change + } + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_put_update_with_invalid_spent_time_hours_only + @request.session[:user_id] = 2 + notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time' + + assert_no_difference('Journal.count') do + put :update, + :id => 1, + :issue => {:notes => notes}, + :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"} + end + assert_response :success + assert_template 'edit' + + assert_error_tag :descendant => {:content => /Activity #{ESCAPED_CANT} be blank/} + assert_select 'textarea[name=?]', 'issue[notes]', :text => notes + assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2z' + end + + def test_put_update_with_invalid_spent_time_comments_only + @request.session[:user_id] = 2 + notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time' + + assert_no_difference('Journal.count') do + put :update, + :id => 1, + :issue => {:notes => notes}, + :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""} + end + assert_response :success + assert_template 'edit' + + assert_error_tag :descendant => {:content => /Activity #{ESCAPED_CANT} be blank/} + assert_error_tag :descendant => {:content => /Hours #{ESCAPED_CANT} be blank/} + assert_select 'textarea[name=?]', 'issue[notes]', :text => notes + assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment' + end + + def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4 + } + + assert_response :redirect + issue.reload + assert_equal 4, issue.fixed_version_id + assert_not_equal issue.project_id, issue.fixed_version.project_id + end + + def test_put_update_should_redirect_back_using_the_back_url_parameter + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4 + }, + :back_url => '/issues' + + assert_response :redirect + assert_redirected_to '/issues' + end + + def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4 + }, + :back_url => 'http://google.com' + + assert_response :redirect + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id + end + + def test_get_bulk_edit + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'ul#bulk-selection' do + assert_select 'li', 2 + assert_select 'li a', :text => 'Bug #1' + end + + assert_select 'form#bulk_edit_form[action=?]', '/issues/bulk_update' do + assert_select 'input[name=?]', 'ids[]', 2 + assert_select 'input[name=?][value=1][type=hidden]', 'ids[]' + + assert_select 'select[name=?]', 'issue[project_id]' + assert_select 'input[name=?]', 'issue[parent_issue_id]' + + # Project specific custom field, date type + field = CustomField.find(9) + assert !field.is_for_all? + assert_equal 'date', field.field_format + assert_select 'input[name=?]', 'issue[custom_field_values][9]' + + # System wide custom field + assert CustomField.find(1).is_for_all? + assert_select 'select[name=?]', 'issue[custom_field_values][1]' + + # Be sure we don't display inactive IssuePriorities + assert ! IssuePriority.find(15).active? + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end + end + end + + def test_get_bulk_edit_on_different_projects + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2, 6] + assert_response :success + assert_template 'bulk_edit' + + # Can not set issues from different projects as children of an issue + assert_select 'input[name=?]', 'issue[parent_issue_id]', 0 + + # Project specific custom field, date type + field = CustomField.find(9) + assert !field.is_for_all? + assert !field.project_ids.include?(Issue.find(6).project_id) + assert_select 'input[name=?]', 'issue[custom_field_values][9]', 0 + end + + def test_get_bulk_edit_with_user_custom_field + field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true) + + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'select.user_cf[name=?]', "issue[custom_field_values][#{field.id}]" do + assert_select 'option', Project.find(1).users.count + 2 # "no change" + "none" options + end + end + + def test_get_bulk_edit_with_version_custom_field + field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true) + + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'select.version_cf[name=?]', "issue[custom_field_values][#{field.id}]" do + assert_select 'option', Project.find(1).shared_versions.count + 2 # "no change" + "none" options + end + end + + def test_get_bulk_edit_with_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + assert_response :success + assert_template 'bulk_edit' + + assert_select 'select[name=?]', 'issue[custom_field_values][1][]' do + assert_select 'option', field.possible_values.size + 1 # "none" options + end + end + + def test_bulk_edit_should_propose_to_clear_text_custom_fields + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 3] + assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', '__none__' + end + + def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 4) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, + :old_status_id => 2, :new_status_id => 1) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, + :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, + :old_status_id => 2, :new_status_id => 5) + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2] + + assert_response :success + statuses = assigns(:available_statuses) + assert_not_nil statuses + assert_equal [1, 3], statuses.map(&:id).sort + + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option', 3 # 2 statuses + "no change" option + end + end + + def test_bulk_edit_should_propose_target_project_open_shared_versions + @request.session[:user_id] = 2 + post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1} + assert_response :success + assert_template 'bulk_edit' + assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort + + assert_select 'select[name=?]', 'issue[fixed_version_id]' do + assert_select 'option', :text => '2.0' + end + end + + def test_bulk_edit_should_propose_target_project_categories + @request.session[:user_id] = 2 + post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1} + assert_response :success + assert_template 'bulk_edit' + assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort + + assert_select 'select[name=?]', 'issue[category_id]' do + assert_select 'option', :text => 'Recipes' + end + end + + def test_bulk_update + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + + assert_response 302 + # check that the issues were updated + assert_equal [7, 7], Issue.where(:id =>[1, 2]).collect {|i| i.priority.id} + + issue = Issue.find(1) + journal = issue.journals.reorder('created_on DESC').first + assert_equal '125', issue.custom_value_for(2).value + assert_equal 'Bulk editing', journal.notes + assert_equal 1, journal.details.size + end + + def test_bulk_update_with_group_assignee + group = Group.find(11) + project = Project.find(1) + project.members << Member.new(:principal => group, :roles => [Role.givable.first]) + + @request.session[:user_id] = 2 + # update issues assignee + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', + :issue => {:priority_id => '', + :assigned_to_id => group.id, + :custom_field_values => {'2' => ''}} + + assert_response 302 + assert_equal [group, group], Issue.where(:id => [1, 2]).collect {|i| i.assigned_to} + end + + def test_bulk_update_on_different_projects + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + + assert_response 302 + # check that the issues were updated + assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id) + + issue = Issue.find(1) + journal = issue.journals.reorder('created_on DESC').first + assert_equal '125', issue.custom_value_for(2).value + assert_equal 'Bulk editing', journal.notes + assert_equal 1, journal.details.size + end + + def test_bulk_update_on_different_projects_without_rights + @request.session[:user_id] = 3 + user = User.find(3) + action = { :controller => "issues", :action => "bulk_update" } + assert user.allowed_to?(action, Issue.find(1).project) + assert ! user.allowed_to?(action, Issue.find(6).project) + post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + assert_response 403 + assert_not_equal "Bulk should fail", Journal.last.notes + end + + def test_bullk_update_should_send_a_notification + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + post(:bulk_update, + { + :ids => [1, 2], + :notes => 'Bulk editing', + :issue => { + :priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''} + } + }) + + assert_response 302 + assert_equal 2, ActionMailer::Base.deliveries.size + end + + def test_bulk_update_project + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'} + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + # Issues moved to project 2 + assert_equal 2, Issue.find(1).project_id + assert_equal 2, Issue.find(2).project_id + # No tracker change + assert_equal 1, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_update_project_on_single_issue_should_follow_when_needed + @request.session[:user_id] = 2 + post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1' + assert_redirected_to '/issues/1' + end + + def test_bulk_update_project_on_multiple_issues_should_follow_when_needed + @request.session[:user_id] = 2 + post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1' + assert_redirected_to '/projects/onlinestore/issues' + end + + def test_bulk_update_tracker + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'} + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 2, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_update_status + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status', + :issue => {:priority_id => '', + :assigned_to_id => '', + :status_id => '5'} + + assert_response 302 + issue = Issue.find(1) + assert issue.closed? + end + + def test_bulk_update_priority + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6} + + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 6, Issue.find(1).priority_id + assert_equal 6, Issue.find(2).priority_id + end + + def test_bulk_update_with_notes + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues' + + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes + assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes + end + + def test_bulk_update_parent_id + IssueRelation.delete_all + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 3], + :notes => 'Bulk editing parent', + :issue => {:priority_id => '', :assigned_to_id => '', + :status_id => '', :parent_issue_id => '2'} + assert_response 302 + parent = Issue.find(2) + assert_equal parent.id, Issue.find(1).parent_id + assert_equal parent.id, Issue.find(3).parent_id + assert_equal [1, 3], parent.children.collect(&:id).sort + end + + def test_bulk_update_custom_field + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'2' => '777'}} + + assert_response 302 + + issue = Issue.find(1) + journal = issue.journals.reorder('created_on DESC').first + assert_equal '777', issue.custom_value_for(2).value + assert_equal 1, journal.details.size + assert_equal '125', journal.details.first.old_value + assert_equal '777', journal.details.first.value + end + + def test_bulk_update_custom_field_to_blank + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'1' => '__none__'}} + assert_response 302 + assert_equal '', Issue.find(1).custom_field_value(1) + assert_equal '', Issue.find(3).custom_field_value(1) + end + + def test_bulk_update_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'1' => ['MySQL', 'Oracle']}} + + assert_response 302 + + assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort + assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort + # the custom field is not associated with the issue tracker + assert_nil Issue.find(2).custom_field_value(1) + end + + def test_bulk_update_multi_custom_field_to_blank + field = CustomField.find(1) + field.update_attribute :multiple, true + + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing multi custom field', + :issue => {:priority_id => '', + :assigned_to_id => '', + :custom_field_values => {'1' => ['__none__']}} + assert_response 302 + assert_equal [''], Issue.find(1).custom_field_value(1) + assert_equal [''], Issue.find(3).custom_field_value(1) + end + + def test_bulk_update_unassign + assert_not_nil Issue.find(2).assigned_to + @request.session[:user_id] = 2 + # unassign issues + post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'} + assert_response 302 + # check that the issues were updated + assert_nil Issue.find(2).assigned_to + end + + def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject + @request.session[:user_id] = 2 + + post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4} + + assert_response :redirect + issues = Issue.find([1,2]) + issues.each do |issue| + assert_equal 4, issue.fixed_version_id + assert_not_equal issue.project_id, issue.fixed_version.project_id + end + end + + def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1,2], :back_url => '/issues' + + assert_response :redirect + assert_redirected_to '/issues' + end + + def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1,2], :back_url => 'http://google.com' + + assert_response :redirect + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier + end + + def test_bulk_update_with_all_failures_should_show_errors + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:start_date => 'foo'} + + assert_response :success + assert_template 'bulk_edit' + assert_select '#errorExplanation span', :text => 'Failed to save 2 issue(s) on 2 selected: #1, #2.' + assert_select '#errorExplanation ul li', :text => 'Start date is not a valid date: #1, #2' + + assert_equal [1, 2], assigns[:issues].map(&:id) + end + + def test_bulk_update_with_some_failures_should_show_errors + issue1 = Issue.generate!(:start_date => '2013-05-12') + issue2 = Issue.generate!(:start_date => '2013-05-15') + issue3 = Issue.generate! + @request.session[:user_id] = 2 + post :bulk_update, :ids => [issue1.id, issue2.id, issue3.id], + :issue => {:due_date => '2013-05-01'} + assert_response :success + assert_template 'bulk_edit' + assert_select '#errorExplanation span', + :text => "Failed to save 2 issue(s) on 3 selected: ##{issue1.id}, ##{issue2.id}." + assert_select '#errorExplanation ul li', + :text => "Due date must be greater than start date: ##{issue1.id}, ##{issue2.id}" + assert_equal [issue1.id, issue2.id], assigns[:issues].map(&:id) + end + + def test_bulk_update_with_failure_should_preserved_form_values + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2', :start_date => 'foo'} + + assert_response :success + assert_template 'bulk_edit' + assert_select 'select[name=?]', 'issue[tracker_id]' do + assert_select 'option[value=2][selected=selected]' + end + assert_select 'input[name=?][value=?]', 'issue[start_date]', 'foo' + end + + def test_get_bulk_copy + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2, 3], :copy => '1' + assert_response :success + assert_template 'bulk_edit' + + issues = assigns(:issues) + assert_not_nil issues + assert_equal [1, 2, 3], issues.map(&:id).sort + + assert_select 'input[name=copy_attachments]' + end + + def test_bulk_copy_to_another_project + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 2 do + assert_no_difference 'Project.find(1).issues.count' do + post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1' + end + end + assert_redirected_to '/projects/ecookbook/issues' + + copies = Issue.order('id DESC').limit(issues.size) + copies.each do |copy| + assert_equal 2, copy.project_id + end + end + + def test_bulk_copy_should_allow_not_changing_the_issue_attributes + @request.session[:user_id] = 2 + issues = [ + Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, + :priority_id => 2, :subject => 'issue 1', :author_id => 1, + :assigned_to_id => nil), + Issue.create!(:project_id => 2, :tracker_id => 3, :status_id => 2, + :priority_id => 1, :subject => 'issue 2', :author_id => 2, + :assigned_to_id => 3) + ] + assert_difference 'Issue.count', issues.size do + post :bulk_update, :ids => issues.map(&:id), :copy => '1', + :issue => { + :project_id => '', :tracker_id => '', :assigned_to_id => '', + :status_id => '', :start_date => '', :due_date => '' + } + end + + copies = Issue.order('id DESC').limit(issues.size) + issues.each do |orig| + copy = copies.detect {|c| c.subject == orig.subject} + assert_not_nil copy + assert_equal orig.project_id, copy.project_id + assert_equal orig.tracker_id, copy.tracker_id + assert_equal orig.status_id, copy.status_id + assert_equal orig.assigned_to_id, copy.assigned_to_id + assert_equal orig.priority_id, copy.priority_id + end + end + + def test_bulk_copy_should_allow_changing_the_issue_attributes + # Fixes random test failure with Mysql + # where Issue.where(:project_id => 2).limit(2).order('id desc') + # doesn't return the expected results + Issue.delete_all("project_id=2") + + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 2 do + assert_no_difference 'Project.find(1).issues.count' do + post :bulk_update, :ids => [1, 2], :copy => '1', + :issue => { + :project_id => '2', :tracker_id => '', :assigned_to_id => '4', + :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31' + } + end + end + + copied_issues = Issue.where(:project_id => 2).limit(2).order('id desc').to_a + assert_equal 2, copied_issues.size + copied_issues.each do |issue| + assert_equal 2, issue.project_id, "Project is incorrect" + assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect" + assert_equal 1, issue.status_id, "Status is incorrect" + assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect" + assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect" + end + end + + def test_bulk_copy_should_allow_adding_a_note + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 1 do + post :bulk_update, :ids => [1], :copy => '1', + :notes => 'Copying one issue', + :issue => { + :project_id => '', :tracker_id => '', :assigned_to_id => '4', + :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31' + } + end + issue = Issue.order('id DESC').first + assert_equal 1, issue.journals.size + journal = issue.journals.first + assert_equal 1, journal.details.size + assert_equal 'Copying one issue', journal.notes + end + + def test_bulk_copy_should_allow_not_copying_the_attachments + attachment_count = Issue.find(3).attachments.size + assert attachment_count > 0 + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 1 do + assert_no_difference 'Attachment.count' do + post :bulk_update, :ids => [3], :copy => '1', + :issue => { + :project_id => '' + } + end + end + end + + def test_bulk_copy_should_allow_copying_the_attachments + attachment_count = Issue.find(3).attachments.size + assert attachment_count > 0 + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 1 do + assert_difference 'Attachment.count', attachment_count do + post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '1', + :issue => { + :project_id => '' + } + end + end + end + + def test_bulk_copy_should_add_relations_with_copied_issues + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 2 do + assert_difference 'IssueRelation.count', 2 do + post :bulk_update, :ids => [1, 3], :copy => '1', + :issue => { + :project_id => '1' + } + end + end + end + + def test_bulk_copy_should_allow_not_copying_the_subtasks + issue = Issue.generate_with_descendants! + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', 1 do + post :bulk_update, :ids => [issue.id], :copy => '1', + :issue => { + :project_id => '' + } + end + end + + def test_bulk_copy_should_allow_copying_the_subtasks + issue = Issue.generate_with_descendants! + count = issue.descendants.count + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', count+1 do + post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '1', + :issue => { + :project_id => '' + } + end + copy = Issue.where(:parent_id => nil).order("id DESC").first + assert_equal count, copy.descendants.count + end + + def test_bulk_copy_should_not_copy_selected_subtasks_twice + issue = Issue.generate_with_descendants! + count = issue.descendants.count + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', count+1 do + post :bulk_update, :ids => issue.self_and_descendants.map(&:id), :copy => '1', :copy_subtasks => '1', + :issue => { + :project_id => '' + } + end + copy = Issue.where(:parent_id => nil).order("id DESC").first + assert_equal count, copy.descendants.count + end + + def test_bulk_copy_to_another_project_should_follow_when_needed + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1' + issue = Issue.order('id DESC').first + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + end + + def test_bulk_copy_with_all_failures_should_display_errors + @request.session[:user_id] = 2 + post :bulk_update, :ids => [1, 2], :copy => '1', :issue => {:start_date => 'foo'} + + assert_response :success + end + + def test_destroy_issue_with_no_time_entries + assert_nil TimeEntry.find_by_issue_id(2) + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -1 do + delete :destroy, :id => 2 + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil Issue.find_by_id(2) + end + + def test_destroy_issues_with_time_entries + @request.session[:user_id] = 2 + + assert_no_difference 'Issue.count' do + delete :destroy, :ids => [1, 3] + end + assert_response :success + assert_template 'destroy' + assert_not_nil assigns(:hours) + assert Issue.find_by_id(1) && Issue.find_by_id(3) + + assert_select 'form' do + assert_select 'input[name=_method][value=delete]' + end + end + + def test_destroy_issues_and_destroy_time_entries + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -2 do + assert_difference 'TimeEntry.count', -3 do + delete :destroy, :ids => [1, 3], :todo => 'destroy' + end + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_nil TimeEntry.find_by_id([1, 2]) + end + + def test_destroy_issues_and_assign_time_entries_to_project + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -2 do + assert_no_difference 'TimeEntry.count' do + delete :destroy, :ids => [1, 3], :todo => 'nullify' + end + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_nil TimeEntry.find(1).issue_id + assert_nil TimeEntry.find(2).issue_id + end + + def test_destroy_issues_and_reassign_time_entries_to_another_issue + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -2 do + assert_no_difference 'TimeEntry.count' do + delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2 + end + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_equal 2, TimeEntry.find(1).issue_id + assert_equal 2, TimeEntry.find(2).issue_id + end + + def test_destroy_issues_from_different_projects + @request.session[:user_id] = 2 + + assert_difference 'Issue.count', -3 do + delete :destroy, :ids => [1, 2, 6], :todo => 'destroy' + end + assert_redirected_to :controller => 'issues', :action => 'index' + assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6)) + end + + def test_destroy_parent_and_child_issues + parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue') + child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id) + assert child.is_descendant_of?(parent.reload) + + @request.session[:user_id] = 2 + assert_difference 'Issue.count', -2 do + delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy' + end + assert_response 302 + end + + def test_destroy_invalid_should_respond_with_404 + @request.session[:user_id] = 2 + assert_no_difference 'Issue.count' do + delete :destroy, :id => 999 + end + assert_response 404 + end + + def test_default_search_scope + get :index + + assert_select 'div#quick-search form' do + assert_select 'input[name=issues][value=1][type=hidden]' + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/37/37f2c260a3ef3c7956ee2bd34621efe201bd4fb9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/37/37f2c260a3ef3c7956ee2bd34621efe201bd4fb9.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,193 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Journal < ActiveRecord::Base + belongs_to :journalized, :polymorphic => true + # added as a quick fix to allow eager loading of the polymorphic association + # since always associated to an issue, for now + belongs_to :issue, :foreign_key => :journalized_id + + belongs_to :user + has_many :details, :class_name => "JournalDetail", :dependent => :delete_all + attr_accessor :indice + + acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, + :description => :notes, + :author => :user, + :group => :issue, + :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} + + acts_as_activity_provider :type => 'issues', + :author_key => :user_id, + :find_options => {:include => [{:issue => :project}, :details, :user], + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + + " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} + + before_create :split_private_notes + after_create :send_notification + + scope :visible, lambda {|*args| + user = args.shift || User.current + + includes(:issue => :project). + where(Issue.visible_condition(user, *args)). + where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) + } + + def save(*args) + # Do not save an empty journal + (details.empty? && notes.blank?) ? false : super + end + + # Returns journal details that are visible to user + def visible_details(user=User.current) + details.select do |detail| + if detail.property == 'cf' + detail.custom_field && detail.custom_field.visible_by?(project, user) + elsif detail.property == 'relation' + Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user) + else + true + end + end + end + + def each_notification(users, &block) + if users.any? + users_by_details_visibility = users.group_by do |user| + visible_details(user) + end + users_by_details_visibility.each do |visible_details, users| + if notes? || visible_details.any? + yield(users) + end + end + end + end + + # Returns the new status if the journal contains a status change, otherwise nil + def new_status + c = details.detect {|detail| detail.prop_key == 'status_id'} + (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil + end + + def new_value_for(prop) + c = details.detect {|detail| detail.prop_key == prop} + c ? c.value : nil + end + + def editable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) + end + + def project + journalized.respond_to?(:project) ? journalized.project : nil + end + + def attachments + journalized.respond_to?(:attachments) ? journalized.attachments : nil + end + + # Returns a string of css classes + def css_classes + s = 'journal' + s << ' has-notes' unless notes.blank? + s << ' has-details' unless details.blank? + s << ' private-notes' if private_notes? + s + end + + def notify? + @notify != false + end + + def notify=(arg) + @notify = arg + end + + def notified_users + notified = journalized.notified_users + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified + end + + def recipients + notified_users.map(&:mail) + end + + def notified_watchers + notified = journalized.notified_watchers + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified + end + + def watcher_recipients + notified_watchers.map(&:mail) + end + + # Sets @custom_field instance variable on journals details using a single query + def self.preload_journals_details_custom_fields(journals) + field_ids = journals.map(&:details).flatten.select {|d| d.property == 'cf'}.map(&:prop_key).uniq + if field_ids.any? + fields_by_id = CustomField.where(:id => field_ids).inject({}) {|h, f| h[f.id] = f; h} + journals.each do |journal| + journal.details.each do |detail| + if detail.property == 'cf' + detail.instance_variable_set "@custom_field", fields_by_id[detail.prop_key.to_i] + end + end + end + end + journals + end + + private + + def split_private_notes + if private_notes? + if notes.present? + if details.any? + # Split the journal (notes/changes) so we don't have half-private journals + journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) + journal.details = details + journal.save + self.details = [] + self.created_on = journal.created_on + end + else + # Blank notes should not be private + self.private_notes = false + end + end + true + end + + def send_notification + if notify? && (Setting.notified_events.include?('issue_updated') || + (Setting.notified_events.include?('issue_note_added') && notes.present?) || + (Setting.notified_events.include?('issue_status_updated') && new_status.present?) || + (Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?) + ) + Mailer.deliver_issue_edit(self) + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/38/385c6750511d0a36b5d915c83580ce155323ad24.svn-base --- a/.svn/pristine/38/385c6750511d0a36b5d915c83580ce155323ad24.svn-base Wed May 07 14:15:02 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1085 +0,0 @@ -# Serbian translations for Redmine -# by Vladimir Medarović (vlada@medarovic.com) -sr-YU: - direction: ltr - jquery: - locale: "sr" - 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: "%d.%m.%Y." - short: "%e %b" - long: "%B %e, %Y" - - day_names: [nedelja, ponedeljak, utorak, sreda, Äetvrtak, petak, subota] - abbr_day_names: [ned, pon, uto, sre, Äet, pet, sub] - - # Don't forget the nil at the beginning; there's no such thing as a 0th month - month_names: [~, januar, februar, mart, april, maj, jun, jul, avgust, septembar, oktobar, novembar, decembar] - abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, avg, sep, okt, nov, dec] - # Used in date_select and datime_select. - order: - - :day - - :month - - :year - - time: - formats: - default: "%d.%m.%Y. u %H:%M" - time: "%H:%M" - short: "%d. %b u %H:%M" - long: "%d. %B %Y u %H:%M" - am: "am" - pm: "pm" - - datetime: - distance_in_words: - half_a_minute: "pola minuta" - less_than_x_seconds: - one: "manje od jedne sekunde" - other: "manje od %{count} sek." - x_seconds: - one: "jedna sekunda" - other: "%{count} sek." - less_than_x_minutes: - one: "manje od minuta" - other: "manje od %{count} min." - x_minutes: - one: "jedan minut" - other: "%{count} min." - about_x_hours: - one: "približno jedan sat" - other: "približno %{count} sati" - x_hours: - one: "1 hour" - other: "%{count} hours" - x_days: - one: "jedan dan" - other: "%{count} dana" - about_x_months: - one: "približno jedan mesec" - other: "približno %{count} meseci" - x_months: - one: "jedan mesec" - other: "%{count} meseci" - about_x_years: - one: "približno godinu dana" - other: "približno %{count} god." - over_x_years: - one: "preko godinu dana" - other: "preko %{count} god." - almost_x_years: - one: "skoro godinu dana" - other: "skoro %{count} god." - - number: - 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: "i" - skip_last_comma: false - - activerecord: - errors: - template: - header: - one: "1 error prohibited this %{model} from being saved" - other: "%{count} errors prohibited this %{model} from being saved" - messages: - inclusion: "nije ukljuÄen u spisak" - exclusion: "je rezervisan" - invalid: "je neispravan" - confirmation: "potvrda ne odgovara" - accepted: "mora biti prihvaćen" - empty: "ne može biti prazno" - blank: "ne može biti prazno" - too_long: "je predugaÄka (maksimum znakova je %{count})" - too_short: "je prekratka (minimum znakova je %{count})" - wrong_length: "je pogreÅ¡ne dužine (broj znakova mora biti %{count})" - taken: "je već u upotrebi" - not_a_number: "nije broj" - not_a_date: "nije ispravan datum" - greater_than: "mora biti veći od %{count}" - greater_than_or_equal_to: "mora biti veći ili jednak %{count}" - equal_to: "mora biti jednak %{count}" - less_than: "mora biti manji od %{count}" - less_than_or_equal_to: "mora biti manji ili jednak %{count}" - odd: "mora biti paran" - even: "mora biti neparan" - greater_than_start_date: "mora biti veći od poÄetnog datuma" - not_same_project: "ne pripada istom projektu" - circular_dependency: "Ova veza će stvoriti kružnu referencu" - cant_link_an_issue_with_a_descendant: "Problem ne može biti povezan sa jednim od svojih podzadataka" - - actionview_instancetag_blank_option: Molim odaberite - - general_text_No: 'Ne' - general_text_Yes: 'Da' - general_text_no: 'ne' - general_text_yes: 'da' - general_lang_name: 'Serbian (Srpski)' - general_csv_separator: ',' - general_csv_decimal_separator: '.' - general_csv_encoding: UTF-8 - general_pdf_encoding: UTF-8 - general_first_day_of_week: '1' - - notice_account_updated: Nalog je uspeÅ¡no ažuriran. - notice_account_invalid_creditentials: Neispravno korisniÄko ime ili lozinka. - notice_account_password_updated: Lozinka je uspeÅ¡no ažurirana. - notice_account_wrong_password: PogreÅ¡na lozinka - notice_account_register_done: KorisniÄki nalog je uspeÅ¡no kreiran. Kliknite na link koji ste dobili u e-poruci za aktivaciju. - notice_account_unknown_email: Nepoznat korisnik. - notice_can_t_change_password: Ovaj korisniÄki nalog za potvrdu identiteta koristi spoljni izvor. Nemoguće je promeniti lozinku. - notice_account_lost_email_sent: Poslata vam je e-poruka sa uputstvom za izbor nove lozinke - notice_account_activated: VaÅ¡ korisniÄki nalog je aktiviran. Sada se možete prijaviti. - notice_successful_create: UspeÅ¡no kreiranje. - notice_successful_update: UspeÅ¡no ažuriranje. - notice_successful_delete: UspeÅ¡no brisanje. - notice_successful_connection: UspeÅ¡no povezivanje. - notice_file_not_found: Strana kojoj želite pristupiti ne postoji ili je uklonjena. - notice_locking_conflict: Podatak je ažuriran od strane drugog korisnika. - notice_not_authorized: Niste ovlašćeni za pristup ovoj strani. - notice_email_sent: "E-poruka je poslata na %{value}" - notice_email_error: "Dogodila se greÅ¡ka prilikom slanja e-poruke (%{value})" - notice_feeds_access_key_reseted: VaÅ¡ RSS pristupni kljuÄ je poniÅ¡ten. - notice_api_access_key_reseted: VaÅ¡ API pristupni kljuÄ je poniÅ¡ten. - notice_failed_to_save_issues: "NeuspeÅ¡no snimanje %{count} problema od %{total} odabranih: %{ids}." - notice_failed_to_save_members: "NeuspeÅ¡no snimanje Älana(ova): %{errors}." - notice_no_issue_selected: "Ni jedan problem nije odabran! Molimo, odaberite problem koji želite da menjate." - notice_account_pending: "VaÅ¡ nalog je kreiran i Äeka na odobrenje administratora." - notice_default_data_loaded: Podrazumevano konfigurisanje je uspeÅ¡no uÄitano. - notice_unable_delete_version: Verziju je nemoguće izbrisati. - notice_unable_delete_time_entry: Stavku evidencije vremena je nemoguće izbrisati. - notice_issue_done_ratios_updated: Odnos reÅ¡enih problema je ažuriran. - - error_can_t_load_default_data: "Podrazumevano konfigurisanje je nemoguće uÄitati: %{value}" - error_scm_not_found: "Stavka ili ispravka nisu pronaÄ‘ene u spremiÅ¡tu." - error_scm_command_failed: "GreÅ¡ka se javila prilikom pokuÅ¡aja pristupa spremiÅ¡tu: %{value}" - error_scm_annotate: "Stavka ne postoji ili ne može biti oznaÄena." - error_issue_not_found_in_project: 'Problem nije pronaÄ‘en ili ne pripada ovom projektu.' - error_no_tracker_in_project: 'Ni jedno praćenje nije povezano sa ovim projektom. Molimo proverite podeÅ¡avanja projekta.' - error_no_default_issue_status: 'Podrazumevani status problema nije definisan. Molimo proverite vaÅ¡e konfigurisanje (idite na "Administracija -> Statusi problema").' - error_can_not_delete_custom_field: Nemoguće je izbrisati prilagoÄ‘eno polje - error_can_not_delete_tracker: "Ovo praćenje sadrži probleme i ne može biti obrisano." - error_can_not_remove_role: "Ova uloga je u upotrebi i ne može biti obrisana." - error_can_not_reopen_issue_on_closed_version: 'Problem dodeljen zatvorenoj verziji ne može biti ponovo otvoren' - error_can_not_archive_project: Ovaj projekat se ne može arhivirati - error_issue_done_ratios_not_updated: "Odnos reÅ¡enih problema nije ažuriran." - error_workflow_copy_source: 'Molimo odaberite izvorno praćenje ili ulogu' - error_workflow_copy_target: 'Molimo odaberite odrediÅ¡no praćenje i ulogu' - error_unable_delete_issue_status: 'Status problema je nemoguće obrisati' - error_unable_to_connect: "Povezivanje sa (%{value}) je nemoguće" - warning_attachments_not_saved: "%{count} datoteka ne može biti snimljena." - - mail_subject_lost_password: "VaÅ¡a %{value} lozinka" - mail_body_lost_password: 'Za promenu vaÅ¡e lozinke, kliknite na sledeći link:' - mail_subject_register: "Aktivacija vaÅ¡eg %{value} naloga" - mail_body_register: 'Za aktivaciju vaÅ¡eg naloga, kliknite na sledeći link:' - mail_body_account_information_external: "VaÅ¡ nalog %{value} možete koristiti za prijavu." - mail_body_account_information: Informacije o vaÅ¡em nalogu - mail_subject_account_activation_request: "Zahtev za aktivaciju naloga %{value}" - mail_body_account_activation_request: "Novi korisnik (%{value}) je registrovan. Nalog Äeka na vaÅ¡e odobrenje:" - mail_subject_reminder: "%{count} problema dospeva narednih %{days} dana" - mail_body_reminder: "%{count} problema dodeljenih vama dospeva u narednih %{days} dana:" - mail_subject_wiki_content_added: "Wiki stranica '%{id}' je dodata" - mail_body_wiki_content_added: "%{author} je dodao wiki stranicu '%{id}'." - mail_subject_wiki_content_updated: "Wiki stranica '%{id}' je ažurirana" - mail_body_wiki_content_updated: "%{author} je ažurirao wiki stranicu '%{id}'." - - gui_validation_error: jedna greÅ¡ka - gui_validation_error_plural: "%{count} greÅ¡aka" - - field_name: Naziv - field_description: Opis - field_summary: Rezime - field_is_required: Obavezno - field_firstname: Ime - field_lastname: Prezime - field_mail: E-adresa - field_filename: Datoteka - field_filesize: VeliÄina - field_downloads: Preuzimanja - field_author: Autor - field_created_on: Kreirano - field_updated_on: Ažurirano - field_field_format: Format - field_is_for_all: Za sve projekte - field_possible_values: Moguće vrednosti - field_regexp: Regularan izraz - field_min_length: Minimalna dužina - field_max_length: Maksimalna dužina - field_value: Vrednost - field_category: Kategorija - field_title: Naslov - field_project: Projekat - field_issue: Problem - field_status: Status - field_notes: BeleÅ¡ke - field_is_closed: Zatvoren problem - field_is_default: Podrazumevana vrednost - field_tracker: Praćenje - field_subject: Predmet - field_due_date: Krajnji rok - field_assigned_to: Dodeljeno - field_priority: Prioritet - field_fixed_version: OdrediÅ¡na verzija - field_user: Korisnik - field_principal: Glavni - field_role: Uloga - field_homepage: PoÄetna stranica - field_is_public: Javno objavljivanje - field_parent: Potprojekat od - field_is_in_roadmap: Problemi prikazani u planu rada - field_login: KorisniÄko ime - field_mail_notification: ObaveÅ¡tenja putem e-poÅ¡te - field_admin: Administrator - field_last_login_on: Poslednje povezivanje - field_language: Jezik - field_effective_date: Datum - field_password: Lozinka - field_new_password: Nova lozinka - field_password_confirmation: Potvrda lozinke - field_version: Verzija - field_type: Tip - field_host: Glavni raÄunar - field_port: Port - field_account: KorisniÄki nalog - field_base_dn: Bazni DN - field_attr_login: Atribut prijavljivanja - field_attr_firstname: Atribut imena - field_attr_lastname: Atribut prezimena - field_attr_mail: Atribut e-adrese - field_onthefly: Kreiranje korisnika u toku rada - field_start_date: PoÄetak - field_done_ratio: "% uraÄ‘eno" - field_auth_source: Režim potvrde identiteta - field_hide_mail: Sakrij moju e-adresu - field_comments: Komentar - field_url: URL - field_start_page: PoÄetna stranica - field_subproject: Potprojekat - field_hours: sati - field_activity: Aktivnost - field_spent_on: Datum - field_identifier: Identifikator - field_is_filter: Upotrebi kao filter - field_issue_to: Srodni problemi - field_delay: KaÅ¡njenje - field_assignable: Problem može biti dodeljen ovoj ulozi - field_redirect_existing_links: Preusmeri postojeće veze - field_estimated_hours: Proteklo vreme - field_column_names: Kolone - field_time_zone: Vremenska zona - field_searchable: Može da se pretražuje - field_default_value: Podrazumevana vrednost - field_comments_sorting: Prikaži komentare - field_parent_title: MatiÄna stranica - field_editable: Izmenljivo - field_watcher: PosmatraÄ - field_identity_url: OpenID URL - field_content: Sadržaj - field_group_by: Grupisanje rezultata po - field_sharing: Deljenje - field_parent_issue: MatiÄni zadatak - - setting_app_title: Naslov aplikacije - setting_app_subtitle: Podnaslov aplikacije - setting_welcome_text: Tekst dobrodoÅ¡lice - setting_default_language: Podrazumevani jezik - setting_login_required: Obavezna potvrda identiteta - setting_self_registration: Samoregistracija - setting_attachment_max_size: Maks. veliÄina priložene datoteke - setting_issues_export_limit: OgraniÄenje izvoza „problema“ - setting_mail_from: E-adresa poÅ¡iljaoca - setting_bcc_recipients: Primaoci „Bcc“ kopije - setting_plain_text_mail: Poruka sa Äistim tekstom (bez HTML-a) - setting_host_name: Putanja i naziv glavnog raÄunara - setting_text_formatting: Oblikovanje teksta - setting_wiki_compression: Kompresija Wiki istorije - setting_feeds_limit: OgraniÄenje sadržaja izvora vesti - setting_default_projects_public: Podrazumeva se javno prikazivanje novih projekata - setting_autofetch_changesets: IzvrÅ¡avanje automatskog preuzimanja - setting_sys_api_enabled: Omogućavanje WS za upravljanje spremiÅ¡tem - setting_commit_ref_keywords: Referenciranje kljuÄnih reÄi - setting_commit_fix_keywords: Popravljanje kljuÄnih reÄi - setting_autologin: Automatska prijava - setting_date_format: Format datuma - setting_time_format: Format vremena - setting_cross_project_issue_relations: Dozvoli povezivanje problema iz unakrsnih projekata - setting_issue_list_default_columns: Podrazumevane kolone prikazane na spisku problema - setting_emails_footer: Podnožje stranice e-poruke - setting_protocol: Protokol - setting_per_page_options: Opcije prikaza objekata po stranici - setting_user_format: Format prikaza korisnika - setting_activity_days_default: Broj dana prikazanih na projektnoj aktivnosti - setting_display_subprojects_issues: Prikazuj probleme iz potprojekata na glavnom projektu, ukoliko nije drugaÄije navedeno - setting_enabled_scm: Omogućavanje SCM - setting_mail_handler_body_delimiters: "Skraćivanje e-poruke nakon jedne od ovih linija" - setting_mail_handler_api_enabled: Omogućavanje WS dolazne e-poruke - setting_mail_handler_api_key: API kljuÄ - setting_sequential_project_identifiers: Generisanje sekvencijalnog imena projekta - setting_gravatar_enabled: Koristi Gravatar korisniÄke ikone - setting_gravatar_default: Podrazumevana Gravatar slika - setting_diff_max_lines_displayed: Maks. broj prikazanih razliÄitih linija - setting_file_max_size_displayed: Maks. veliÄina tekst. datoteka prikazanih umetnuto - setting_repository_log_display_limit: Maks. broj revizija prikazanih u datoteci za evidenciju - setting_openid: Dozvoli OpenID prijavu i registraciju - setting_password_min_length: Minimalna dužina lozinke - setting_new_project_user_role_id: Kreatoru projekta (koji nije administrator) dodeljuje je uloga - setting_default_projects_modules: Podrazumevano omogućeni moduli za nove projekte - setting_issue_done_ratio: IzraÄunaj odnos reÅ¡enih problema - setting_issue_done_ratio_issue_field: koristeći polje problema - setting_issue_done_ratio_issue_status: koristeći status problema - setting_start_of_week: Prvi dan u sedmici - setting_rest_api_enabled: Omogući REST web usluge - setting_cache_formatted_text: KeÅ¡iranje obraÄ‘enog teksta - - permission_add_project: Kreiranje projekta - permission_add_subprojects: Kreiranje potpojekta - permission_edit_project: Izmena projekata - permission_select_project_modules: Odabiranje modula projekta - permission_manage_members: Upravljanje Älanovima - permission_manage_project_activities: Upravljanje projektnim aktivnostima - permission_manage_versions: Upravljanje verzijama - permission_manage_categories: Upravljanje kategorijama problema - permission_view_issues: Pregled problema - permission_add_issues: Dodavanje problema - permission_edit_issues: Izmena problema - permission_manage_issue_relations: Upravljanje vezama izmeÄ‘u problema - permission_add_issue_notes: Dodavanje beleÅ¡ki - permission_edit_issue_notes: Izmena beleÅ¡ki - permission_edit_own_issue_notes: Izmena sopstvenih beleÅ¡ki - permission_move_issues: Pomeranje problema - permission_delete_issues: Brisanje problema - permission_manage_public_queries: Upravljanje javnim upitima - permission_save_queries: Snimanje upita - permission_view_gantt: Pregledanje Gantovog dijagrama - permission_view_calendar: Pregledanje kalendara - permission_view_issue_watchers: Pregledanje spiska posmatraÄa - permission_add_issue_watchers: Dodavanje posmatraÄa - permission_delete_issue_watchers: Brisanje posmatraÄa - permission_log_time: Beleženje utroÅ¡enog vremena - permission_view_time_entries: Pregledanje utroÅ¡enog vremena - permission_edit_time_entries: Izmena utroÅ¡enog vremena - permission_edit_own_time_entries: Izmena sopstvenog utroÅ¡enog vremena - permission_manage_news: Upravljanje vestima - permission_comment_news: Komentarisanje vesti - permission_manage_documents: Upravljanje dokumentima - permission_view_documents: Pregledanje dokumenata - permission_manage_files: Upravljanje datotekama - permission_view_files: Pregledanje datoteka - permission_manage_wiki: Upravljanje wiki stranicama - permission_rename_wiki_pages: Promena imena wiki stranicama - permission_delete_wiki_pages: Brisanje wiki stranica - permission_view_wiki_pages: Pregledanje wiki stranica - permission_view_wiki_edits: Pregledanje wiki istorije - permission_edit_wiki_pages: Izmena wiki stranica - permission_delete_wiki_pages_attachments: Brisanje priloženih datoteka - permission_protect_wiki_pages: ZaÅ¡tita wiki stranica - permission_manage_repository: Upravljanje spremiÅ¡tem - permission_browse_repository: Pregledanje spremiÅ¡ta - permission_view_changesets: Pregledanje skupa promena - permission_commit_access: Potvrda pristupa - permission_manage_boards: Upravljanje forumima - permission_view_messages: Pregledanje poruka - permission_add_messages: Slanje poruka - permission_edit_messages: Izmena poruka - permission_edit_own_messages: Izmena sopstvenih poruka - permission_delete_messages: Brisanje poruka - permission_delete_own_messages: Brisanje sopstvenih poruka - permission_export_wiki_pages: Izvoz wiki stranica - permission_manage_subtasks: Upravljanje podzadacima - - project_module_issue_tracking: Praćenje problema - project_module_time_tracking: Praćenje vremena - project_module_news: Vesti - project_module_documents: Dokumenti - project_module_files: Datoteke - project_module_wiki: Wiki - project_module_repository: SpremiÅ¡te - project_module_boards: Forumi - - label_user: Korisnik - label_user_plural: Korisnici - label_user_new: Novi korisnik - label_user_anonymous: Anoniman - label_project: Projekat - label_project_new: Novi projekat - label_project_plural: Projekti - label_x_projects: - zero: nema projekata - one: jedan projekat - other: "%{count} projekata" - label_project_all: Svi projekti - label_project_latest: Poslednji projekti - label_issue: Problem - label_issue_new: Novi problem - label_issue_plural: Problemi - label_issue_view_all: Prikaz svih problema - label_issues_by: "Problemi (%{value})" - label_issue_added: Problem je dodat - label_issue_updated: Problem je ažuriran - label_document: Dokument - label_document_new: Novi dokument - label_document_plural: Dokumenti - label_document_added: Dokument je dodat - label_role: Uloga - label_role_plural: Uloge - label_role_new: Nova uloga - label_role_and_permissions: Uloge i dozvole - label_member: ÄŒlan - label_member_new: Novi Älan - label_member_plural: ÄŒlanovi - label_tracker: Praćenje - label_tracker_plural: Praćenja - label_tracker_new: Novo praćenje - label_workflow: Tok posla - label_issue_status: Status problema - label_issue_status_plural: Statusi problema - label_issue_status_new: Novi status - label_issue_category: Kategorija problema - label_issue_category_plural: Kategorije problema - label_issue_category_new: Nova kategorija - label_custom_field: PrilagoÄ‘eno polje - label_custom_field_plural: PrilagoÄ‘ena polja - label_custom_field_new: Novo prilagoÄ‘eno polje - label_enumerations: Nabrojiva lista - label_enumeration_new: Nova vrednost - label_information: Informacija - label_information_plural: Informacije - label_please_login: Molimo, prijavite se - label_register: Registracija - label_login_with_open_id_option: ili prijava sa OpenID - label_password_lost: Izgubljena lozinka - label_home: PoÄetak - label_my_page: Moja stranica - label_my_account: Moj nalog - label_my_projects: Moji projekti - label_my_page_block: My page block - label_administration: Administracija - label_login: Prijava - label_logout: Odjava - label_help: Pomoć - label_reported_issues: Prijavljeni problemi - label_assigned_to_me_issues: Problemi dodeljeni meni - label_last_login: Poslednje povezivanje - label_registered_on: Registrovan - label_activity: Aktivnost - label_overall_activity: Celokupna aktivnost - label_user_activity: "Aktivnost korisnika %{value}" - label_new: Novo - label_logged_as: Prijavljeni ste kao - label_environment: Okruženje - label_authentication: Potvrda identiteta - label_auth_source: Režim potvrde identiteta - label_auth_source_new: Novi režim potvrde identiteta - label_auth_source_plural: Režimi potvrde identiteta - label_subproject_plural: Potprojekti - label_subproject_new: Novi potprojekat - label_and_its_subprojects: "%{value} i njegovi potprojekti" - label_min_max_length: Min. - Maks. dužina - label_list: Spisak - label_date: Datum - label_integer: Ceo broj - label_float: Sa pokretnim zarezom - label_boolean: LogiÄki operator - label_string: Tekst - label_text: Dugi tekst - label_attribute: Osobina - label_attribute_plural: Osobine - label_download: "%{count} preuzimanje" - label_download_plural: "%{count} preuzimanja" - label_no_data: Nema podataka za prikazivanje - label_change_status: Promena statusa - label_history: Istorija - label_attachment: Datoteka - label_attachment_new: Nova datoteka - label_attachment_delete: Brisanje datoteke - label_attachment_plural: Datoteke - label_file_added: Datoteka je dodata - label_report: IzveÅ¡taj - label_report_plural: IzveÅ¡taji - label_news: Vesti - label_news_new: Dodavanje vesti - label_news_plural: Vesti - label_news_latest: Poslednje vesti - label_news_view_all: Prikaz svih vesti - label_news_added: Vesti su dodate - label_settings: PodeÅ¡avanja - label_overview: Pregled - label_version: Verzija - label_version_new: Nova verzija - label_version_plural: Verzije - label_close_versions: Zatvori zavrÅ¡ene verzije - label_confirmation: Potvrda - label_export_to: 'TakoÄ‘e dostupno i u varijanti:' - label_read: ÄŒitanje... - label_public_projects: Javni projekti - label_open_issues: otvoren - label_open_issues_plural: otvorenih - label_closed_issues: zatvoren - label_closed_issues_plural: zatvorenih - label_x_open_issues_abbr_on_total: - zero: 0 otvorenih / %{total} - one: 1 otvoren / %{total} - other: "%{count} otvorenih / %{total}" - label_x_open_issues_abbr: - zero: 0 otvorenih - one: 1 otvoren - other: "%{count} otvorenih" - label_x_closed_issues_abbr: - zero: 0 zatvorenih - one: 1 zatvoren - other: "%{count} zatvorenih" - label_total: Ukupno - label_permissions: Dozvole - label_current_status: Trenutni status - label_new_statuses_allowed: Novi statusi dozvoljeni - label_all: svi - label_none: nijedan - label_nobody: nikome - label_next: Sledeće - label_previous: Prethodno - label_used_by: Koristio - label_details: Detalji - label_add_note: Dodaj beleÅ¡ku - label_per_page: Po strani - label_calendar: Kalendar - label_months_from: meseci od - label_gantt: Gantov dijagram - label_internal: UnutraÅ¡nji - label_last_changes: "poslednjih %{count} promena" - label_change_view_all: Prikaži sve promene - label_personalize_page: Personalizuj ovu stranu - label_comment: Komentar - label_comment_plural: Komentari - label_x_comments: - zero: bez komentara - one: jedan komentar - other: "%{count} komentara" - label_comment_add: Dodaj komentar - label_comment_added: Komentar dodat - label_comment_delete: ObriÅ¡i komentare - label_query: PrilagoÄ‘en upit - label_query_plural: PrilagoÄ‘eni upiti - label_query_new: Novi upit - label_filter_add: Dodavanje filtera - label_filter_plural: Filteri - label_equals: je - label_not_equals: nije - label_in_less_than: manje od - label_in_more_than: viÅ¡e od - label_greater_or_equal: '>=' - label_less_or_equal: '<=' - label_in: u - label_today: danas - label_all_time: sve vreme - label_yesterday: juÄe - label_this_week: ove sedmice - label_last_week: poslednje sedmice - label_last_n_days: "poslednjih %{count} dana" - label_this_month: ovog meseca - label_last_month: poslednjeg meseca - label_this_year: ove godine - label_date_range: Vremenski period - label_less_than_ago: pre manje od nekoliko dana - label_more_than_ago: pre viÅ¡e od nekoliko dana - label_ago: pre nekoliko dana - label_contains: sadrži - label_not_contains: ne sadrži - label_day_plural: dana - label_repository: SpremiÅ¡te - label_repository_plural: SpremiÅ¡ta - label_browse: Pregledanje - label_modification: "%{count} promena" - label_modification_plural: "%{count} promena" - label_branch: Grana - label_tag: Oznaka - label_revision: Revizija - label_revision_plural: Revizije - label_revision_id: "Revizija %{value}" - label_associated_revisions: Pridružene revizije - label_added: dodato - label_modified: promenjeno - label_copied: kopirano - label_renamed: preimenovano - label_deleted: izbrisano - label_latest_revision: Poslednja revizija - label_latest_revision_plural: Poslednje revizije - label_view_revisions: Pregled revizija - label_view_all_revisions: Pregled svih revizija - label_max_size: Maksimalna veliÄina - label_sort_highest: PremeÅ¡tanje na vrh - label_sort_higher: PremeÅ¡tanje na gore - label_sort_lower: PremeÅ¡tanje na dole - label_sort_lowest: PremeÅ¡tanje na dno - label_roadmap: Plan rada - label_roadmap_due_in: "Dospeva %{value}" - label_roadmap_overdue: "%{value} najkasnije" - label_roadmap_no_issues: Nema problema za ovu verziju - label_search: Pretraga - label_result_plural: Rezultati - label_all_words: Sve reÄi - label_wiki: Wiki - label_wiki_edit: Wiki izmena - label_wiki_edit_plural: Wiki izmene - label_wiki_page: Wiki stranica - label_wiki_page_plural: Wiki stranice - label_index_by_title: Indeksiranje po naslovu - label_index_by_date: Indeksiranje po datumu - label_current_version: Trenutna verzija - label_preview: Pregled - label_feed_plural: Izvori vesti - label_changes_details: Detalji svih promena - label_issue_tracking: Praćenje problema - label_spent_time: UtroÅ¡eno vreme - label_overall_spent_time: Celokupno utroÅ¡eno vreme - label_f_hour: "%{value} sat" - label_f_hour_plural: "%{value} sati" - label_time_tracking: Praćenje vremena - label_change_plural: Promene - label_statistics: Statistika - label_commits_per_month: IzvrÅ¡enja meseÄno - label_commits_per_author: IzvrÅ¡enja po autoru - label_view_diff: Pogledaj razlike - label_diff_inline: unutra - label_diff_side_by_side: uporedo - label_options: Opcije - label_copy_workflow_from: Kopiranje toka posla od - label_permissions_report: IzveÅ¡taj o dozvolama - label_watched_issues: Posmatrani problemi - label_related_issues: Srodni problemi - label_applied_status: Primenjeni statusi - label_loading: UÄitavanje... - label_relation_new: Nova relacija - label_relation_delete: Brisanje relacije - label_relates_to: srodnih sa - label_duplicates: dupliranih - label_duplicated_by: dupliranih od - label_blocks: odbijenih - label_blocked_by: odbijenih od - label_precedes: prethodi - label_follows: praćenih - label_end_to_start: od kraja do poÄetka - label_end_to_end: od kraja do kraja - label_start_to_start: od poÄetka do poÄetka - label_start_to_end: od poÄetka do kraja - label_stay_logged_in: Ostanite prijavljeni - label_disabled: onemogućeno - label_show_completed_versions: Prikazivanje zavrÅ¡ene verzije - label_me: meni - label_board: Forum - label_board_new: Novi forum - label_board_plural: Forumi - label_board_locked: ZakljuÄana - label_board_sticky: Lepljiva - label_topic_plural: Teme - label_message_plural: Poruke - label_message_last: Poslednja poruka - label_message_new: Nova poruka - label_message_posted: Poruka je dodata - label_reply_plural: Odgovori - label_send_information: PoÅ¡alji korisniku detalje naloga - label_year: Godina - label_month: Mesec - label_week: Sedmica - label_date_from: Å alje - label_date_to: Prima - label_language_based: Bazirano na jeziku korisnika - label_sort_by: "Sortirano po %{value}" - label_send_test_email: Slanje probne e-poruke - label_feeds_access_key: RSS pristupni kljuÄ - label_missing_feeds_access_key: RSS pristupni kljuÄ nedostaje - label_feeds_access_key_created_on: "RSS pristupni kljuÄ je napravljen pre %{value}" - label_module_plural: Moduli - label_added_time_by: "Dodao %{author} pre %{age}" - label_updated_time_by: "Ažurirao %{author} pre %{age}" - label_updated_time: "Ažurirano pre %{value}" - label_jump_to_a_project: Skok na projekat... - label_file_plural: Datoteke - label_changeset_plural: Skupovi promena - label_default_columns: Podrazumevane kolone - label_no_change_option: (Bez promena) - label_bulk_edit_selected_issues: Grupna izmena odabranih problema - label_theme: Tema - label_default: Podrazumevano - label_search_titles_only: Pretražuj samo naslove - label_user_mail_option_all: "Za bilo koji dogaÄ‘aj na svim mojim projektima" - label_user_mail_option_selected: "Za bilo koji dogaÄ‘aj na samo odabranim projektima..." - label_user_mail_no_self_notified: "Ne želim biti obaveÅ¡tavan za promene koje sam pravim" - label_registration_activation_by_email: aktivacija naloga putem e-poruke - label_registration_manual_activation: ruÄna aktivacija naloga - label_registration_automatic_activation: automatska aktivacija naloga - label_display_per_page: "Broj stavki po stranici: %{value}" - label_age: Starost - label_change_properties: Promeni svojstva - label_general: OpÅ¡ti - label_more: ViÅ¡e - label_scm: SCM - label_plugins: Dodatne komponente - label_ldap_authentication: LDAP potvrda identiteta - label_downloads_abbr: D/L - label_optional_description: Opciono opis - label_add_another_file: Dodaj joÅ¡ jednu datoteku - label_preferences: PodeÅ¡avanja - label_chronological_order: po hronoloÅ¡kom redosledu - label_reverse_chronological_order: po obrnutom hronoloÅ¡kom redosledu - label_planning: Planiranje - label_incoming_emails: Dolazne e-poruke - label_generate_key: Generisanje kljuÄa - label_issue_watchers: PosmatraÄi - label_example: Primer - label_display: Prikaz - label_sort: Sortiranje - label_ascending: Rastući niz - label_descending: Opadajući niz - label_date_from_to: Od %{start} do %{end} - label_wiki_content_added: Wiki stranica je dodata - label_wiki_content_updated: Wiki stranica je ažurirana - label_group: Grupa - label_group_plural: Grupe - label_group_new: Nova grupa - label_time_entry_plural: UtroÅ¡eno vreme - label_version_sharing_none: Nije deljeno - label_version_sharing_descendants: Sa potprojektima - label_version_sharing_hierarchy: Sa hijerarhijom projekta - label_version_sharing_tree: Sa stablom projekta - label_version_sharing_system: Sa svim projektima - label_update_issue_done_ratios: Ažuriraj odnos reÅ¡enih problema - label_copy_source: Izvor - label_copy_target: OdrediÅ¡te - label_copy_same_as_target: Isto kao odrediÅ¡te - label_display_used_statuses_only: Prikazuj statuse korišćene samo od strane ovog praćenja - label_api_access_key: API pristupni kljuÄ - label_missing_api_access_key: Nedostaje API pristupni kljuÄ - label_api_access_key_created_on: "API pristupni kljuÄ je kreiran pre %{value}" - label_profile: Profil - label_subtask_plural: Podzadatak - label_project_copy_notifications: PoÅ¡alji e-poruku sa obaveÅ¡tenjem prilikom kopiranja projekta - - button_login: Prijava - button_submit: PoÅ¡alji - button_save: Snimi - button_check_all: UkljuÄi sve - button_uncheck_all: IskljuÄi sve - button_delete: IzbriÅ¡i - button_create: Kreiraj - button_create_and_continue: Kreiraj i nastavi - button_test: Test - button_edit: Izmeni - button_add: Dodaj - button_change: Promeni - button_apply: Primeni - button_clear: ObriÅ¡i - button_lock: ZakljuÄaj - button_unlock: OtkljuÄaj - button_download: Preuzmi - button_list: Spisak - button_view: Prikaži - button_move: Pomeri - button_move_and_follow: Pomeri i prati - button_back: Nazad - button_cancel: PoniÅ¡ti - button_activate: Aktiviraj - button_sort: Sortiraj - button_log_time: Evidentiraj vreme - button_rollback: Povratak na ovu verziju - button_watch: Prati - button_unwatch: Ne prati viÅ¡e - button_reply: Odgovori - button_archive: Arhiviraj - button_unarchive: Vrati iz arhive - button_reset: PoniÅ¡ti - button_rename: Preimenuj - button_change_password: Promeni lozinku - button_copy: Kopiraj - button_copy_and_follow: Kopiraj i prati - button_annotate: Pribeleži - button_update: Ažuriraj - button_configure: Podesi - button_quote: Pod navodnicima - button_duplicate: Dupliraj - button_show: Prikaži - - status_active: aktivni - status_registered: registrovani - status_locked: zakljuÄani - - version_status_open: otvoren - version_status_locked: zakljuÄan - version_status_closed: zatvoren - - field_active: Aktivan - - text_select_mail_notifications: Odaberi akcije za koje će obaveÅ¡tenje biti poslato putem e-poÅ¡te. - text_regexp_info: npr. ^[A-Z0-9]+$ - text_min_max_length_info: 0 znaÄi bez ograniÄenja - text_project_destroy_confirmation: Jeste li sigurni da želite da izbriÅ¡ete ovaj projekat i sve pripadajuće podatke? - text_subprojects_destroy_warning: "Potprojekti: %{value} će takoÄ‘e biti izbrisan." - text_workflow_edit: Odaberite ulogu i praćenje za izmenu toka posla - text_are_you_sure: Jeste li sigurni? - text_journal_changed: "%{label} promenjen od %{old} u %{new}" - text_journal_set_to: "%{label} postavljen u %{value}" - text_journal_deleted: "%{label} izbrisano (%{old})" - text_journal_added: "%{label} %{value} dodato" - text_tip_issue_begin_day: zadatak poÄinje ovog dana - text_tip_issue_end_day: zadatak se zavrÅ¡ava ovog dana - text_tip_issue_begin_end_day: zadatak poÄinje i zavrÅ¡ava ovog dana - text_caracters_maximum: "NajviÅ¡e %{count} znak(ova)." - text_caracters_minimum: "Broj znakova mora biti najmanje %{count}." - text_length_between: "Broj znakova mora biti izmeÄ‘u %{min} i %{max}." - text_tracker_no_workflow: Ovo praćenje nema definisan tok posla - text_unallowed_characters: Nedozvoljeni znakovi - text_comma_separated: Dozvoljene su viÅ¡estruke vrednosti (odvojene zarezom). - text_line_separated: Dozvoljene su viÅ¡estruke vrednosti (jedan red za svaku vrednost). - text_issues_ref_in_commit_messages: Referenciranje i popravljanje problema u izvrÅ¡nim porukama - text_issue_added: "%{author} je prijavio problem %{id}." - text_issue_updated: "%{author} je ažurirao problem %{id}." - text_wiki_destroy_confirmation: Jeste li sigurni da želite da obriÅ¡ete wiki i sav sadržaj? - text_issue_category_destroy_question: "Nekoliko problema (%{count}) je dodeljeno ovoj kategoriji. Å ta želite da uradite?" - text_issue_category_destroy_assignments: Ukloni dodeljene kategorije - text_issue_category_reassign_to: Dodeli ponovo probleme ovoj kategoriji - text_user_mail_option: "Za neizabrane projekte, dobićete samo obaveÅ¡tenje o stvarima koje pratite ili ste ukljuÄeni (npr. problemi Äiji ste vi autor ili zastupnik)." - text_no_configuration_data: "Uloge, praćenja, statusi problema i toka posla joÅ¡ uvek nisu podeÅ¡eni.\nPreporuÄljivo je da uÄitate podrazumevano konfigurisanje. Izmena je moguća nakon prvog uÄitavanja." - text_load_default_configuration: UÄitaj podrazumevano konfigurisanje - text_status_changed_by_changeset: "Primenjeno u skupu sa promenama %{value}." - text_issues_destroy_confirmation: 'Jeste li sigurni da želite da izbriÅ¡ete odabrane probleme?' - text_select_project_modules: 'Odaberite module koje želite omogućiti za ovaj projekat:' - text_default_administrator_account_changed: Podrazumevani administratorski nalog je promenjen - text_file_repository_writable: Fascikla priloženih datoteka je upisiva - text_plugin_assets_writable: Fascikla elemenata dodatnih komponenti je upisiva - text_rmagick_available: RMagick je dostupan (opciono) - text_destroy_time_entries_question: "%{hours} sati je prijavljeno za ovaj problem koji želite izbrisati. Å ta želite da uradite?" - text_destroy_time_entries: IzbriÅ¡i prijavljene sate - text_assign_time_entries_to_project: Dodeli prijavljene sate projektu - text_reassign_time_entries: 'Dodeli ponovo prijavljene sate ovom problemu:' - text_user_wrote: "%{value} je napisao:" - text_enumeration_destroy_question: "%{count} objekat(a) je dodeljeno ovoj vrednosti." - text_enumeration_category_reassign_to: 'Dodeli ih ponovo ovoj vrednosti:' - text_email_delivery_not_configured: "Isporuka e-poruka nije konfigurisana i obaveÅ¡tenja su onemogućena.\nPodesite vaÅ¡ SMTP server u config/configuration.yml i pokrenite ponovo aplikaciju za njihovo omogućavanje." - text_repository_usernames_mapping: "Odaberite ili ažurirajte Redmine korisnike mapiranjem svakog korisniÄkog imena pronaÄ‘enog u evidenciji spremiÅ¡ta.\nKorisnici sa istim Redmine imenom i imenom spremiÅ¡ta ili e-adresom su automatski mapirani." - text_diff_truncated: '... Ova razlika je iseÄena jer je dostignuta maksimalna veliÄina prikaza.' - text_custom_field_possible_values_info: 'Jedan red za svaku vrednost' - text_wiki_page_destroy_question: "Ova stranica ima %{descendants} podreÄ‘enih stranica i podstranica. Å ta želite da uradite?" - text_wiki_page_nullify_children: "Zadrži podreÄ‘ene stranice kao korene stranice" - text_wiki_page_destroy_children: "IzbriÅ¡i podreÄ‘ene stranice i sve njihove podstranice" - text_wiki_page_reassign_children: "Dodeli ponovo podreÄ‘ene stranice ovoj matiÄnoj stranici" - text_own_membership_delete_confirmation: "Nakon uklanjanja pojedinih ili svih vaÅ¡ih dozvola nećete viÅ¡e moći da ureÄ‘ujete ovaj projekat.\nŽelite li da nastavite?" - text_zoom_in: Uvećaj - text_zoom_out: Umanji - - default_role_manager: Menadžer - default_role_developer: Programer - default_role_reporter: IzveÅ¡taÄ - default_tracker_bug: GreÅ¡ka - default_tracker_feature: Funkcionalnost - default_tracker_support: PodrÅ¡ka - default_issue_status_new: Novo - default_issue_status_in_progress: U toku - default_issue_status_resolved: ReÅ¡eno - default_issue_status_feedback: Povratna informacija - default_issue_status_closed: Zatvoreno - default_issue_status_rejected: Odbijeno - default_doc_category_user: KorisniÄka dokumentacija - default_doc_category_tech: TehniÄka dokumentacija - default_priority_low: Nizak - default_priority_normal: Normalan - default_priority_high: Visok - default_priority_urgent: Hitno - default_priority_immediate: Neposredno - default_activity_design: Dizajn - default_activity_development: Razvoj - - enumeration_issue_priorities: Prioriteti problema - enumeration_doc_categories: Kategorije dokumenta - enumeration_activities: Aktivnosti (praćenje vremena) - enumeration_system_activity: Sistemska aktivnost - - field_time_entries: Vreme evidencije - project_module_gantt: Gantov dijagram - project_module_calendar: Kalendar - button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" - field_text: Text field - label_user_mail_option_only_owner: Samo za stvari koje posedujem - setting_default_notification_option: Podrazumevana opcija za notifikaciju - label_user_mail_option_only_my_events: Za dogadjaje koje pratim ili sam u njih ukljuÄen - label_user_mail_option_only_assigned: Za dogadjaje koji su mi dodeljeni liÄno - label_user_mail_option_none: Bez obaveÅ¡tenja - field_member_of_group: Assignee's group - field_assigned_to_role: Assignee's role - notice_not_authorized_archived_project: Projekat kome pokuÅ¡avate da pristupite je arhiviran - label_principal_search: "Traži korisnike ili grupe:" - label_user_search: "Traži korisnike:" - field_visible: Vidljivo - setting_emails_header: Email zaglavlje - setting_commit_logtime_activity_id: Activity for logged time - text_time_logged_by_changeset: Applied in changeset %{value}. - setting_commit_logtime_enabled: Omogući praćenje vremena - notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) - setting_gantt_items_limit: Maksimalan broj stavki na gant grafiku - field_warn_on_leaving_unsaved: Upozori me ako napuÅ¡tam stranu sa tekstom koji nije snimljen - text_warn_on_leaving_unsaved: Strana sadrži tekst koji nije snimljen i biće izgubljen ako je napustite. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} ažuriran" - label_news_comment_added: Komentar dodat u novosti - button_expand_all: ProÅ¡iri sve - button_collapse_all: Zatvori sve - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Da li ste sigurni da želite da obriÅ¡ete selektovane stavke ? - label_role_anonymous: Anonimus - label_role_non_member: Nije Älan - label_issue_note_added: Nota dodana - label_issue_status_updated: Status ažuriran - label_issue_priority_updated: Prioritet ažuriran - label_issues_visibility_own: Problem kreiran od strane ili je dodeljen korisniku - field_issues_visibility: Vidljivost problema - label_issues_visibility_all: Svi problemi - permission_set_own_issues_private: Podesi sopstveni problem kao privatan ili javan - field_is_private: Privatno - permission_set_issues_private: Podesi problem kao privatan ili javan - label_issues_visibility_public: Svi javni problemi - text_issues_destroy_descendants_confirmation: Ova operacija će takoÄ‘e obrisati %{count} podzadataka. - field_commit_logs_encoding: Kodiranje izvrÅ¡nih poruka - 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 - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - 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 problem - one: 1 problem - other: "%{count} problemi" - 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.
    Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account - text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project - label_any: svi - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: Sa potprojektima - label_cross_project_tree: Sa stablom projekta - label_cross_project_hierarchy: Sa hijerarhijom projekta - label_cross_project_system: Sa svim projektima - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/38/385f3eab1c979f41797640419fbb6f558b89820f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/38/385f3eab1c979f41797640419fbb6f558b89820f.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,306 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 RepositoryBazaarTest < ActiveSupport::TestCase + fixtures :projects + + include Redmine::I18n + + REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s + REPOSITORY_PATH_TRUNK = File.join(REPOSITORY_PATH, "trunk") + NUM_REV = 4 + + REPOSITORY_PATH_NON_ASCII = Rails.root.join(REPOSITORY_PATH + '/' + 'non_ascii').to_s + + # Bazaar core does not support xml output such as Subversion and Mercurial. + # "bzr" command output and command line parameter depend on locale. + # So, non ASCII path tests cannot run independent locale. + # + # If you want to run Bazaar non ASCII path tests on Linux *Ruby 1.9*, + # you need to set locale character set "ISO-8859-1". + # E.g. "LANG=en_US.ISO-8859-1". + # On Linux other platforms (e.g. Ruby 1.8, JRuby), + # you need to set "RUN_LATIN1_OUTPUT_TEST = true" manually. + # + # On Windows, because it is too hard to change system locale, + # you cannot run Bazaar non ASCII path tests. + # + RUN_LATIN1_OUTPUT_TEST = (RUBY_PLATFORM != 'java' && + REPOSITORY_PATH.respond_to?(:force_encoding) && + Encoding.locale_charmap == "ISO-8859-1") + + CHAR_1_UTF8_HEX = "\xc3\x9c" + CHAR_1_LATIN1_HEX = "\xdc" + + def setup + @project = Project.find(3) + @repository = Repository::Bazaar.create( + :project => @project, :url => REPOSITORY_PATH_TRUNK, + :log_encoding => 'UTF-8') + assert @repository + @char_1_utf8 = CHAR_1_UTF8_HEX.dup + @char_1_ascii8bit = CHAR_1_LATIN1_HEX.dup + if @char_1_utf8.respond_to?(:force_encoding) + @char_1_utf8.force_encoding('UTF-8') + @char_1_ascii8bit.force_encoding('ASCII-8BIT') + end + end + + def test_blank_path_to_repository_error_message + set_language_if_valid 'en' + repo = Repository::Bazaar.new( + :project => @project, + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include "Path to repository can't be blank", + repo.errors.full_messages + end + + def test_blank_path_to_repository_error_message_fr + set_language_if_valid 'fr' + str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + repo = Repository::Bazaar.new( + :project => @project, + :url => "", + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include str, repo.errors.full_messages + end + + if File.directory?(REPOSITORY_PATH_TRUNK) + def test_fetch_changesets_from_scratch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + + assert_equal NUM_REV, @repository.changesets.count + assert_equal 9, @repository.filechanges.count + assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments + end + + def test_fetch_changesets_incremental + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + # Remove changesets with revision > 5 + @repository.changesets.all.each {|c| c.destroy if c.revision.to_i > 2} + @project.reload + assert_equal 2, @repository.changesets.count + + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + end + + def test_entries + entries = @repository.entries + assert_kind_of Redmine::Scm::Adapters::Entries, entries + assert_equal 2, entries.size + + assert_equal 'dir', entries[0].kind + assert_equal 'directory', entries[0].name + assert_equal 'directory', entries[0].path + + assert_equal 'file', entries[1].kind + assert_equal 'doc-mkdir.txt', entries[1].name + assert_equal 'doc-mkdir.txt', entries[1].path + end + + def test_entries_in_subdirectory + entries = @repository.entries('directory') + assert_equal 3, entries.size + + assert_equal 'file', entries.last.kind + assert_equal 'edit.png', entries.last.name + assert_equal 'directory/edit.png', entries.last.path + end + + def test_previous + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('3') + assert_equal @repository.find_changeset_by_name('2'), changeset.previous + end + + def test_previous_nil + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('1') + assert_nil changeset.previous + end + + def test_next + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('2') + assert_equal @repository.find_changeset_by_name('3'), changeset.next + end + + def test_next_nil + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + changeset = @repository.find_changeset_by_name('4') + assert_nil changeset.next + end + + if File.directory?(REPOSITORY_PATH_NON_ASCII) && RUN_LATIN1_OUTPUT_TEST + def test_cat_latin1_path + latin1_repo = create_latin1_repo + buf = latin1_repo.cat( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2) + assert buf + lines = buf.split("\n") + assert_equal 2, lines.length + assert_equal 'It is written in Python.', lines[1] + + buf = latin1_repo.cat( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2) + assert buf + lines = buf.split("\n") + assert_equal 1, lines.length + assert_equal "test-#{@char_1_ascii8bit}.txt", lines[0] + end + + def test_annotate_latin1_path + latin1_repo = create_latin1_repo + ann1 = latin1_repo.annotate( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2) + assert_equal 2, ann1.lines.size + assert_equal '2', ann1.revisions[0].identifier + assert_equal 'test00@', ann1.revisions[0].author + assert_equal 'It is written in Python.', ann1.lines[1] + ann2 = latin1_repo.annotate( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2) + assert_equal 1, ann2.lines.size + assert_equal '2', ann2.revisions[0].identifier + assert_equal 'test00@', ann2.revisions[0].author + assert_equal "test-#{@char_1_ascii8bit}.txt", ann2.lines[0] + end + + def test_diff_latin1_path + latin1_repo = create_latin1_repo + diff1 = latin1_repo.diff( + "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2, 1) + assert_equal 7, diff1.size + buf = diff1[5].gsub(/\r\n|\r|\n/, "") + assert_equal "+test-#{@char_1_ascii8bit}.txt", buf + end + + def test_entries_latin1_path + latin1_repo = create_latin1_repo + entries = latin1_repo.entries("test-#{@char_1_utf8}-dir", 2) + assert_kind_of Redmine::Scm::Adapters::Entries, entries + assert_equal 3, entries.size + assert_equal 'file', entries[1].kind + assert_equal "test-#{@char_1_utf8}-1.txt", entries[0].name + assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", entries[0].path + end + + def test_entry_latin1_path + latin1_repo = create_latin1_repo + ["test-#{@char_1_utf8}-dir", + "/test-#{@char_1_utf8}-dir", + "/test-#{@char_1_utf8}-dir/" + ].each do |path| + entry = latin1_repo.entry(path, 2) + assert_equal "test-#{@char_1_utf8}-dir", entry.path + assert_equal "dir", entry.kind + end + ["test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", + "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt" + ].each do |path| + entry = latin1_repo.entry(path, 2) + assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", + entry.path + assert_equal "file", entry.kind + end + end + + def test_changeset_latin1_path + latin1_repo = create_latin1_repo + assert_equal 0, latin1_repo.changesets.count + latin1_repo.fetch_changesets + @project.reload + assert_equal 3, latin1_repo.changesets.count + + cs2 = latin1_repo.changesets.find_by_revision('2') + assert_not_nil cs2 + assert_equal "test-#{@char_1_utf8}", cs2.comments + c2 = cs2.filechanges.sort_by(&:path) + assert_equal 4, c2.size + assert_equal 'A', c2[0].action + assert_equal "/test-#{@char_1_utf8}-dir/", c2[0].path + assert_equal 'A', c2[1].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", c2[1].path + assert_equal 'A', c2[2].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c2[2].path + assert_equal 'A', c2[3].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c2[3].path + + cs3 = latin1_repo.changesets.find_by_revision('3') + assert_not_nil cs3 + assert_equal "modify, move and delete #{@char_1_utf8} files", cs3.comments + c3 = cs3.filechanges.sort_by(&:path) + assert_equal 3, c3.size + assert_equal 'M', c3[0].action + assert_equal "/test-#{@char_1_utf8}-1.txt", c3[0].path + assert_equal 'D', c3[1].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c3[1].path + assert_equal 'M', c3[2].action + assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c3[2].path + end + else + msg = "Bazaar non ASCII output test cannot run this environment." + "\n" + if msg.respond_to?(:force_encoding) + msg += "Encoding.locale_charmap: " + Encoding.locale_charmap + "\n" + end + puts msg + end + + private + + def create_latin1_repo + repo = Repository::Bazaar.create( + :project => @project, + :identifier => 'latin1', + :url => REPOSITORY_PATH_NON_ASCII, + :log_encoding => 'ISO-8859-1' + ) + assert repo + repo + end + else + puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/38/38794a755bb76dee707c7e93753e5d0343705ce5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/38/38794a755bb76dee707c7e93753e5d0343705ce5.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,55 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 'action_view/helpers/form_helper' + +class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder + include Redmine::I18n + + (field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for check_box) + + %w(date_select)).each do |selector| + src = <<-END_SRC + def #{selector}(field, options = {}) + label_for_field(field, options) + super(field, options.except(:label)).html_safe + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def check_box(field, options={}, checked_value="1", unchecked_value="0") + label_for_field(field, options) + super(field, options.except(:label), checked_value, unchecked_value).html_safe + end + + def select(field, choices, options = {}, html_options = {}) + label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe + end + + def time_zone_select(field, priority_zones = nil, options = {}, html_options = {}) + label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe + end + + # Returns a label tag for the given field + def label_for_field(field, options = {}) + return ''.html_safe if options.delete(:no_label) + text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label] + text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) + text += @template.content_tag("span", " *", :class => "required") if options.delete(:required) + @template.content_tag("label", text.html_safe, + :class => (@object && @object.errors[field].present? ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/38/387c7328dffa5d4f176a177e772369b5a7044e34.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/38/387c7328dffa5d4f176a177e772369b5a7044e34.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,149 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::ApiTest::AttachmentsTest < Redmine::ApiTest::Base + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :attachments + + def setup + Setting.rest_api_enabled = '1' + set_fixtures_attachments_directory + end + + def teardown + set_tmp_attachments_directory + end + + test "GET /attachments/:id.xml should return the attachment" do + get '/attachments/7.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'attachment', + :child => { + :tag => 'id', + :content => '7', + :sibling => { + :tag => 'filename', + :content => 'archive.zip', + :sibling => { + :tag => 'content_url', + :content => 'http://www.example.com/attachments/download/7/archive.zip' + } + } + } + end + + test "GET /attachments/:id.xml should deny access without credentials" do + get '/attachments/7.xml' + assert_response 401 + set_tmp_attachments_directory + end + + test "GET /attachments/download/:id/:filename should return the attachment content" do + get '/attachments/download/7/archive.zip', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + set_tmp_attachments_directory + end + + test "GET /attachments/download/:id/:filename should deny access without credentials" do + get '/attachments/download/7/archive.zip' + assert_response 302 + set_tmp_attachments_directory + end + + test "POST /uploads.xml should return the token" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.xml', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + assert_equal 'application/xml', response.content_type + end + + xml = Hash.from_xml(response.body) + assert_kind_of Hash, xml['upload'] + token = xml['upload']['token'] + assert_not_nil token + + attachment = Attachment.first(:order => 'id DESC') + assert_equal token, attachment.token + assert_nil attachment.container + assert_equal 2, attachment.author_id + assert_equal 'File content'.size, attachment.filesize + assert attachment.content_type.blank? + assert attachment.filename.present? + assert_match /\d+_[0-9a-z]+/, attachment.diskfile + assert File.exist?(attachment.diskfile) + assert_equal 'File content', File.read(attachment.diskfile) + end + + test "POST /uploads.json should return the token" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.json', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + assert_equal 'application/json', response.content_type + end + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json['upload'] + token = json['upload']['token'] + assert_not_nil token + + attachment = Attachment.first(:order => 'id DESC') + assert_equal token, attachment.token + end + + test "POST /uploads.xml should accept :filename param as the attachment filename" do + set_tmp_attachments_directory + assert_difference 'Attachment.count' do + post '/uploads.xml?filename=test.txt', 'File content', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + + attachment = Attachment.order('id DESC').first + assert_equal 'test.txt', attachment.filename + assert_match /_test\.txt$/, attachment.diskfile + end + + test "POST /uploads.xml should not accept other content types" do + set_tmp_attachments_directory + assert_no_difference 'Attachment.count' do + post '/uploads.xml', 'PNG DATA', {"CONTENT_TYPE" => 'image/png'}.merge(credentials('jsmith')) + assert_response 406 + end + end + + test "POST /uploads.xml should return errors if file is too big" do + set_tmp_attachments_directory + with_settings :attachment_max_size => 1 do + assert_no_difference 'Attachment.count' do + post '/uploads.xml', ('x' * 2048), {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response 422 + assert_tag 'error', :content => /exceeds the maximum allowed file size/ + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/38/38cb9cea714e4d54066f4f27ed2bd0f760f4fd27.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/38/38cb9cea714e4d54066f4f27ed2bd0f760f4fd27.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,12 @@ +class RemoveEolsFromAttachmentsFilename < ActiveRecord::Migration + def up + Attachment.where("filename like ? or filename like ?", "%\r%", "%\n%").each do |attachment| + filename = attachment.filename.to_s.tr("\r\n", "_") + Attachment.where(:id => attachment.id).update_all(:filename => filename) + end + end + + def down + # nop + end +end diff -r d98d22a98252 -r fb9a13467253 .svn/pristine/39/391379aa6687c1c76f9f32c56002bdc88e1847cf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/39/391379aa6687c1c76f9f32c56002bdc88e1847cf.svn-base Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,614 @@ +/* Redmine - project management software + Copyright (C) 2006-2014 Jean-Philippe Lang */ + +function checkAll(id, checked) { + $('#'+id).find('input[type=checkbox]:enabled').attr('checked', checked); +} + +function toggleCheckboxesBySelector(selector) { + var all_checked = true; + $(selector).each(function(index) { + if (!$(this).is(':checked')) { all_checked = false; } + }); + $(selector).attr('checked', !all_checked); +} + +function showAndScrollTo(id, focus) { + $('#'+id).show(); + if (focus !== null) { + $('#'+focus).focus(); + } + $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100); +} + +function toggleRowGroup(el) { + var tr = $(el).parents('tr').first(); + var n = tr.next(); + tr.toggleClass('open'); + while (n.length && !n.hasClass('group')) { + n.toggle(); + n = n.next('tr'); + } +} + +function collapseAllRowGroups(el) { + var tbody = $(el).parents('tbody').first(); + tbody.children('tr').each(function(index) { + if ($(this).hasClass('group')) { + $(this).removeClass('open'); + } else { + $(this).hide(); + } + }); +} + +function expandAllRowGroups(el) { + var tbody = $(el).parents('tbody').first(); + tbody.children('tr').each(function(index) { + if ($(this).hasClass('group')) { + $(this).addClass('open'); + } else { + $(this).show(); + } + }); +} + +function toggleAllRowGroups(el) { + var tr = $(el).parents('tr').first(); + if (tr.hasClass('open')) { + collapseAllRowGroups(el); + } else { + expandAllRowGroups(el); + } +} + +function toggleFieldset(el) { + var fieldset = $(el).parents('fieldset').first(); + fieldset.toggleClass('collapsed'); + fieldset.children('div').toggle(); +} + +function hideFieldset(el) { + var fieldset = $(el).parents('fieldset').first(); + fieldset.toggleClass('collapsed'); + fieldset.children('div').hide(); +} + +function initFilters() { + $('#add_filter_select').change(function() { + addFilter($(this).val(), '', []); + }); + $('#filters-table td.field input[type=checkbox]').each(function() { + toggleFilter($(this).val()); + }); + $('#filters-table td.field input[type=checkbox]').live('click', function() { + toggleFilter($(this).val()); + }); + $('#filters-table .toggle-multiselect').live('click', function() { + toggleMultiSelect($(this).siblings('select')); + }); + $('#filters-table input[type=text]').live('keypress', function(e) { + if (e.keyCode == 13) submit_query_form("query_form"); + }); +} + +function addFilter(field, operator, values) { + var fieldId = field.replace('.', '_'); + var tr = $('#tr_'+fieldId); + if (tr.length > 0) { + tr.show(); + } else { + buildFilterRow(field, operator, values); + } + $('#cb_'+fieldId).attr('checked', true); + toggleFilter(field); + $('#add_filter_select').val('').children('option').each(function() { + if ($(this).attr('value') == field) { + $(this).attr('disabled', true); + } + }); +} + +function buildFilterRow(field, operator, values) { + var fieldId = field.replace('.', '_'); + var filterTable = $("#filters-table"); + var filterOptions = availableFilters[field]; + if (!filterOptions) return; + var operators = operatorByType[filterOptions['type']]; + var filterValues = filterOptions['values']; + var i, select; + + var tr = $('').attr('id', 'tr_'+fieldId).html( + '' + + '' + + '  ' + ); + select = tr.find('td.values select'); + if (values.length > 1) { select.attr('multiple', true); } + for (i = 0; i < filterValues.length; i++) { + var filterValue = filterValues[i]; + var option = $('
    RAW + result1 = link_to("CookBook documentation", + "/projects/ecookbook/wiki/CookBook_documentation", + :class => "wiki-page") + result2 = link_to('#1', + "/issues/1", + :class => Issue.find(1).css_classes, + :title => "Can't print recipes (New)") + expected = <<-EXPECTED -

    CookBook documentation

    -

    #1

    +

    #{result1}

    +

    #{result2}

     [[CookBook documentation]]
     
    @@ -790,13 +1002,15 @@
       end
     
       def test_wiki_links_in_tables
    -    to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
    -                 'Link title' +
    -                 'Other title' +
    -                 'Cell 21Last page'
    -    }
    +    text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
    +    link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
    +    link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
    +    link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
    +    result = "#{link1}" +
    +               "#{link2}" +
    +               "Cell 21#{link3}"
         @project = Project.find(1)
    -    to_test.each { |text, result| assert_equal "#{result}
    ", textilizable(text).gsub(/[\t\n]/, '') } + assert_equal "#{result}
    ", textilizable(text).gsub(/[\t\n]/, '') end def test_text_formatting @@ -961,6 +1175,24 @@ assert textilizable(raw).gsub("\n", "").include?(expected) end + def test_toc_with_textile_formatting_should_be_parsed + with_settings :text_formatting => 'textile' do + assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading' + assert_select_in textilizable("{{ 'Heading' + assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading' + end + end + + if Object.const_defined?(:Redcarpet) + def test_toc_with_markdown_formatting_should_be_parsed + with_settings :text_formatting => 'markdown' do + assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading' + assert_select_in textilizable("{{ 'Heading' + assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading' + end + end + end + def test_section_edit_links raw = <<-RAW h1. Title @@ -989,14 +1221,14 @@ result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "") # heading that contains inline code - assert_match Regexp.new('
    ' + + assert_match Regexp.new('
    ' + 'Edit
    ' + '' + '

    Subtitle with inline code

    '), result # last heading - assert_match Regexp.new('
    ' + + assert_match Regexp.new('
    ' + 'Edit
    ' + '' + '

    Subtitle after pre tag

    '), @@ -1050,7 +1282,8 @@ def test_link_to_user user = User.find(2) - assert_equal 'John Smith', link_to_user(user) + result = link_to("John Smith", "/users/2", :class => "user active") + assert_equal result, link_to_user(user) end def test_link_to_user_should_not_link_to_locked_user @@ -1065,7 +1298,8 @@ with_current_user User.find(1) do user = User.find(5) assert user.locked? - assert_equal 'Dave2 Lopper2', link_to_user(user) + result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked") + assert_equal result, link_to_user(user) end end @@ -1076,6 +1310,27 @@ assert_equal ::I18n.t(:label_user_anonymous), t end + def test_link_to_attachment + a = Attachment.find(3) + assert_equal 'logo.gif', + link_to_attachment(a) + assert_equal 'Text', + link_to_attachment(a, :text => 'Text') + result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo") + assert_equal result, + link_to_attachment(a, :class => 'foo') + assert_equal 'logo.gif', + link_to_attachment(a, :download => true) + assert_equal 'logo.gif', + link_to_attachment(a, :only_path => false) + end + + def test_thumbnail_tag + a = Attachment.find(3) + assert_equal '3', + thumbnail_tag(a) + end + def test_link_to_project project = Project.find(1) assert_equal %(eCookbook), @@ -1084,14 +1339,25 @@ link_to_project(project, :action => 'settings') assert_equal %(eCookbook), link_to_project(project, {:only_path => false, :jump => 'blah'}) - assert_equal %(eCookbook), + result = link_to("eCookbook", "/projects/ecookbook/settings", :class => "project") + assert_equal result, link_to_project(project, {:action => 'settings'}, :class => "project") end + def test_link_to_project_settings + project = Project.find(1) + assert_equal 'eCookbook', link_to_project_settings(project) + + project.status = Project::STATUS_CLOSED + assert_equal 'eCookbook', link_to_project_settings(project) + + project.status = Project::STATUS_ARCHIVED + assert_equal 'eCookbook', link_to_project_settings(project) + end + def test_link_to_legacy_project_with_numerical_identifier_should_use_id # numeric identifier are no longer allowed - Project.update_all "identifier=25", "id=1" - + Project.where(:id => 1).update_all(:identifier => 25) assert_equal 'eCookbook', link_to_project(Project.find(1)) end @@ -1164,18 +1430,87 @@ assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo) end - def test_per_page_links_should_show_usefull_values - set_language_if_valid 'en' - stubs(:link_to).returns("[link]") + def test_raw_json_should_escape_closing_tags + s = raw_json(["bar"]) + assert_equal '["bar<\/foo>"]', s + end - with_settings :per_page_options => '10, 25, 50, 100' do - assert_nil per_page_links(10, 3) - assert_nil per_page_links(25, 3) - assert_equal "Per page: 10, [link]", per_page_links(10, 22) - assert_equal "Per page: [link], 25", per_page_links(25, 22) - assert_equal "Per page: [link], [link], 50", per_page_links(50, 22) - assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26) - assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120) - end + def test_raw_json_should_be_html_safe + s = raw_json(["foo"]) + assert s.html_safe? + end + + def test_html_title_should_app_title_if_not_set + assert_equal 'Redmine', html_title + end + + def test_html_title_should_join_items + html_title 'Foo', 'Bar' + assert_equal 'Foo - Bar - Redmine', html_title + end + + def test_html_title_should_append_current_project_name + @project = Project.find(1) + html_title 'Foo', 'Bar' + assert_equal 'Foo - Bar - eCookbook - Redmine', html_title + end + + def test_title_should_return_a_h2_tag + assert_equal '

    Foo

    ', title('Foo') + end + + def test_title_should_set_html_title + title('Foo') + assert_equal 'Foo - Redmine', html_title + end + + def test_title_should_turn_arrays_into_links + assert_equal '

    Foo

    ', title(['Foo', '/foo']) + assert_equal 'Foo - Redmine', html_title + end + + def test_title_should_join_items + assert_equal '

    Foo » Bar

    ', title('Foo', 'Bar') + assert_equal 'Bar - Foo - Redmine', html_title + end + + def test_favicon_path + assert_match %r{^/favicon\.ico}, favicon_path + end + + def test_favicon_path_with_suburi + Redmine::Utils.relative_url_root = '/foo' + assert_match %r{^/foo/favicon\.ico}, favicon_path + ensure + Redmine::Utils.relative_url_root = '' + end + + def test_favicon_url + assert_match %r{^http://test\.host/favicon\.ico}, favicon_url + end + + def test_favicon_url_with_suburi + Redmine::Utils.relative_url_root = '/foo' + assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url + ensure + Redmine::Utils.relative_url_root = '' + end + + def test_truncate_single_line + str = "01234" + result = truncate_single_line_raw("#{str}\n#{str}", 10) + assert_equal "01234 0...", result + assert !result.html_safe? + result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16) + assert_equal "01234<&#> 012...", result + assert !result.html_safe? + end + + def test_truncate_single_line_non_ascii + ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10) + assert_equal "#{ja} #{ja}...", result + assert !result.html_safe? end end diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/custom_fields_helper_test.rb --- a/test/unit/helpers/custom_fields_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/custom_fields_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,14 +18,27 @@ require File.expand_path('../../../test_helper', __FILE__) class CustomFieldsHelperTest < ActionView::TestCase + include ApplicationHelper include CustomFieldsHelper include Redmine::I18n include ERB::Util def test_format_boolean_value I18n.locale = 'en' - assert_equal 'Yes', format_value('1', 'bool') - assert_equal 'No', format_value('0', 'bool') + assert_equal 'Yes', format_value('1', CustomField.new(:field_format => 'bool')) + assert_equal 'No', format_value('0', CustomField.new(:field_format => 'bool')) + end + + def test_label_tag_should_include_description_as_span_title_if_present + field = CustomField.new(:field_format => 'string', :description => 'This is the description') + tag = custom_field_label_tag('foo', CustomValue.new(:custom_field => field)) + assert_select_in tag, 'label span[title=?]', 'This is the description' + end + + def test_label_tag_should_not_include_title_if_description_is_blank + field = CustomField.new(:field_format => 'string') + tag = custom_field_label_tag('foo', CustomValue.new(:custom_field => field)) + assert_select_in tag, 'label span[title]', 0 end def test_unknow_field_format_should_be_edited_as_string @@ -41,7 +54,7 @@ field = CustomField.new(:field_format => 'foo') field.id = 52 - assert_equal '', + assert_include '', custom_field_tag_for_bulk_edit('object', field) end end diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/issues_helper_test.rb --- a/test/unit/helpers/issues_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/issues_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,10 +18,11 @@ require File.expand_path('../../../test_helper', __FILE__) class IssuesHelperTest < ActionView::TestCase - include ApplicationHelper + include Redmine::I18n include IssuesHelper include CustomFieldsHelper include ERB::Util + include Rails.application.routes.url_helpers fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, @@ -30,7 +31,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :custom_fields, :attachments, :versions @@ -46,182 +46,230 @@ end def test_issues_destroy_confirmation_message_with_one_root_issue - assert_equal l(:text_issues_destroy_confirmation), issues_destroy_confirmation_message(Issue.find(1)) + assert_equal l(:text_issues_destroy_confirmation), + issues_destroy_confirmation_message(Issue.find(1)) end def test_issues_destroy_confirmation_message_with_an_arrayt_of_root_issues - assert_equal l(:text_issues_destroy_confirmation), issues_destroy_confirmation_message(Issue.find([1, 2])) + assert_equal l(:text_issues_destroy_confirmation), + issues_destroy_confirmation_message(Issue.find([1, 2])) end def test_issues_destroy_confirmation_message_with_one_parent_issue Issue.find(2).update_attribute :parent_issue_id, 1 - assert_equal l(:text_issues_destroy_confirmation) + "\n" + l(:text_issues_destroy_descendants_confirmation, :count => 1), - issues_destroy_confirmation_message(Issue.find(1)) + assert_equal l(:text_issues_destroy_confirmation) + "\n" + + l(:text_issues_destroy_descendants_confirmation, :count => 1), + issues_destroy_confirmation_message(Issue.find(1)) end def test_issues_destroy_confirmation_message_with_one_parent_issue_and_its_child Issue.find(2).update_attribute :parent_issue_id, 1 - assert_equal l(:text_issues_destroy_confirmation), issues_destroy_confirmation_message(Issue.find([1, 2])) + assert_equal l(:text_issues_destroy_confirmation), + issues_destroy_confirmation_message(Issue.find([1, 2])) end - context "IssuesHelper#show_detail" do - context "with no_html" do - should 'show a changing attribute' do - @detail = JournalDetail.new(:property => 'attr', :old_value => '40', :value => '100', :prop_key => 'done_ratio') - assert_equal "% Done changed from 40 to 100", show_detail(@detail, true) - end + test 'show_detail with no_html should show a changing attribute' do + detail = JournalDetail.new(:property => 'attr', :old_value => '40', + :value => '100', :prop_key => 'done_ratio') + assert_equal "% Done changed from 40 to 100", show_detail(detail, true) + end - should 'show a new attribute' do - @detail = JournalDetail.new(:property => 'attr', :old_value => nil, :value => '100', :prop_key => 'done_ratio') - assert_equal "% Done set to 100", show_detail(@detail, true) - end + test 'show_detail with no_html should show a new attribute' do + detail = JournalDetail.new(:property => 'attr', :old_value => nil, + :value => '100', :prop_key => 'done_ratio') + assert_equal "% Done set to 100", show_detail(detail, true) + end - should 'show a deleted attribute' do - @detail = JournalDetail.new(:property => 'attr', :old_value => '50', :value => nil, :prop_key => 'done_ratio') - assert_equal "% Done deleted (50)", show_detail(@detail, true) - end - end + test 'show_detail with no_html should show a deleted attribute' do + detail = JournalDetail.new(:property => 'attr', :old_value => '50', + :value => nil, :prop_key => 'done_ratio') + assert_equal "% Done deleted (50)", show_detail(detail, true) + end - context "with html" do - should 'show a changing attribute with HTML highlights' do - @detail = JournalDetail.new(:property => 'attr', :old_value => '40', :value => '100', :prop_key => 'done_ratio') - html = show_detail(@detail, false) + test 'show_detail with html should show a changing attribute with HTML highlights' do + detail = JournalDetail.new(:property => 'attr', :old_value => '40', + :value => '100', :prop_key => 'done_ratio') + html = show_detail(detail, false) + assert_include '% Done', html + assert_include '40', html + assert_include '100', html + end - assert_include '% Done', html - assert_include '40', html - assert_include '100', html - end + test 'show_detail with html should show a new attribute with HTML highlights' do + detail = JournalDetail.new(:property => 'attr', :old_value => nil, + :value => '100', :prop_key => 'done_ratio') + html = show_detail(detail, false) + assert_include '% Done', html + assert_include '100', html + end - should 'show a new attribute with HTML highlights' do - @detail = JournalDetail.new(:property => 'attr', :old_value => nil, :value => '100', :prop_key => 'done_ratio') - html = show_detail(@detail, false) + test 'show_detail with html should show a deleted attribute with HTML highlights' do + detail = JournalDetail.new(:property => 'attr', :old_value => '50', + :value => nil, :prop_key => 'done_ratio') + html = show_detail(detail, false) + assert_include '% Done', html + assert_include '50', html + end - assert_include '% Done', html - assert_include '100', html - end - - should 'show a deleted attribute with HTML highlights' do - @detail = JournalDetail.new(:property => 'attr', :old_value => '50', :value => nil, :prop_key => 'done_ratio') - html = show_detail(@detail, false) - - assert_include '% Done', html - assert_include '50', html - end - end - - context "with a start_date attribute" do - should "format the current date" do - @detail = JournalDetail.new( - :property => 'attr', - :old_value => '2010-01-01', - :value => '2010-01-31', - :prop_key => 'start_date' - ) - with_settings :date_format => '%m/%d/%Y' do - assert_match "01/31/2010", show_detail(@detail, true) - end - end - - should "format the old date" do - @detail = JournalDetail.new( - :property => 'attr', - :old_value => '2010-01-01', - :value => '2010-01-31', - :prop_key => 'start_date' - ) - with_settings :date_format => '%m/%d/%Y' do - assert_match "01/01/2010", show_detail(@detail, true) - end - end - end - - context "with a due_date attribute" do - should "format the current date" do - @detail = JournalDetail.new( - :property => 'attr', - :old_value => '2010-01-01', - :value => '2010-01-31', - :prop_key => 'due_date' - ) - with_settings :date_format => '%m/%d/%Y' do - assert_match "01/31/2010", show_detail(@detail, true) - end - end - - should "format the old date" do - @detail = JournalDetail.new( - :property => 'attr', - :old_value => '2010-01-01', - :value => '2010-01-31', - :prop_key => 'due_date' - ) - with_settings :date_format => '%m/%d/%Y' do - assert_match "01/01/2010", show_detail(@detail, true) - end - end - end - - should "show old and new values with a project attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'project_id', :old_value => 1, :value => 2) - assert_match 'eCookbook', show_detail(detail, true) - assert_match 'OnlineStore', show_detail(detail, true) - end - - should "show old and new values with a issue status attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'status_id', :old_value => 1, :value => 2) - assert_match 'New', show_detail(detail, true) - assert_match 'Assigned', show_detail(detail, true) - end - - should "show old and new values with a tracker attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'tracker_id', :old_value => 1, :value => 2) - assert_match 'Bug', show_detail(detail, true) - assert_match 'Feature request', show_detail(detail, true) - end - - should "show old and new values with a assigned to attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'assigned_to_id', :old_value => 1, :value => 2) - assert_match 'redMine Admin', show_detail(detail, true) - assert_match 'John Smith', show_detail(detail, true) - end - - should "show old and new values with a priority attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'priority_id', :old_value => 4, :value => 5) - assert_match 'Low', show_detail(detail, true) - assert_match 'Normal', show_detail(detail, true) - end - - should "show old and new values with a category attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'category_id', :old_value => 1, :value => 2) - assert_match 'Printing', show_detail(detail, true) - assert_match 'Recipes', show_detail(detail, true) - end - - should "show old and new values with a fixed version attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'fixed_version_id', :old_value => 1, :value => 2) - assert_match '0.1', show_detail(detail, true) - assert_match '1.0', show_detail(detail, true) - end - - should "show old and new values with a estimated hours attribute" do - detail = JournalDetail.new(:property => 'attr', :prop_key => 'estimated_hours', :old_value => '5', :value => '6.3') - assert_match '5.00', show_detail(detail, true) - assert_match '6.30', show_detail(detail, true) - end - - should "show old and new values with a custom field" do - detail = JournalDetail.new(:property => 'cf', :prop_key => '1', :old_value => 'MySQL', :value => 'PostgreSQL') - assert_equal 'Database changed from MySQL to PostgreSQL', show_detail(detail, true) - end - - should "show added file" do - detail = JournalDetail.new(:property => 'attachment', :prop_key => '1', :old_value => nil, :value => 'error281.txt') - assert_match 'error281.txt', show_detail(detail, true) - end - - should "show removed file" do - detail = JournalDetail.new(:property => 'attachment', :prop_key => '1', :old_value => 'error281.txt', :value => nil) - assert_match 'error281.txt', show_detail(detail, true) + test 'show_detail with a start_date attribute should format the dates' do + detail = JournalDetail.new( + :property => 'attr', + :old_value => '2010-01-01', + :value => '2010-01-31', + :prop_key => 'start_date' + ) + with_settings :date_format => '%m/%d/%Y' do + assert_match "01/31/2010", show_detail(detail, true) + assert_match "01/01/2010", show_detail(detail, true) end end + + test 'show_detail with a due_date attribute should format the dates' do + detail = JournalDetail.new( + :property => 'attr', + :old_value => '2010-01-01', + :value => '2010-01-31', + :prop_key => 'due_date' + ) + with_settings :date_format => '%m/%d/%Y' do + assert_match "01/31/2010", show_detail(detail, true) + assert_match "01/01/2010", show_detail(detail, true) + end + end + + test 'show_detail should show old and new values with a project attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'project_id', + :old_value => 1, :value => 2) + assert_match 'eCookbook', show_detail(detail, true) + assert_match 'OnlineStore', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a issue status attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'status_id', + :old_value => 1, :value => 2) + assert_match 'New', show_detail(detail, true) + assert_match 'Assigned', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a tracker attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'tracker_id', + :old_value => 1, :value => 2) + assert_match 'Bug', show_detail(detail, true) + assert_match 'Feature request', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a assigned to attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'assigned_to_id', + :old_value => 1, :value => 2) + assert_match 'Redmine Admin', show_detail(detail, true) + assert_match 'John Smith', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a priority attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'priority_id', + :old_value => 4, :value => 5) + assert_match 'Low', show_detail(detail, true) + assert_match 'Normal', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a category attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'category_id', + :old_value => 1, :value => 2) + assert_match 'Printing', show_detail(detail, true) + assert_match 'Recipes', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a fixed version attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'fixed_version_id', + :old_value => 1, :value => 2) + assert_match '0.1', show_detail(detail, true) + assert_match '1.0', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a estimated hours attribute' do + detail = JournalDetail.new(:property => 'attr', :prop_key => 'estimated_hours', + :old_value => '5', :value => '6.3') + assert_match '5.00', show_detail(detail, true) + assert_match '6.30', show_detail(detail, true) + end + + test 'show_detail should show old and new values with a custom field' do + detail = JournalDetail.new(:property => 'cf', :prop_key => '1', + :old_value => 'MySQL', :value => 'PostgreSQL') + assert_equal 'Database changed from MySQL to PostgreSQL', show_detail(detail, true) + end + + test 'show_detail should show added file' do + detail = JournalDetail.new(:property => 'attachment', :prop_key => '1', + :old_value => nil, :value => 'error281.txt') + assert_match 'error281.txt', show_detail(detail, true) + end + + test 'show_detail should show removed file' do + detail = JournalDetail.new(:property => 'attachment', :prop_key => '1', + :old_value => 'error281.txt', :value => nil) + assert_match 'error281.txt', show_detail(detail, true) + end + + def test_show_detail_relation_added + detail = JournalDetail.new(:property => 'relation', + :prop_key => 'precedes', + :value => 1) + assert_equal "Precedes Bug #1: Can't print recipes added", show_detail(detail, true) + str = link_to("Bug #1", "/issues/1", :class => Issue.find(1).css_classes) + assert_equal "Precedes #{str}: #{ESCAPED_UCANT} print recipes added", + show_detail(detail, false) + end + + def test_show_detail_relation_added_with_inexistant_issue + inexistant_issue_number = 9999 + assert_nil Issue.find_by_id(inexistant_issue_number) + detail = JournalDetail.new(:property => 'relation', + :prop_key => 'precedes', + :value => inexistant_issue_number) + assert_equal "Precedes Issue ##{inexistant_issue_number} added", show_detail(detail, true) + assert_equal "Precedes Issue ##{inexistant_issue_number} added", show_detail(detail, false) + end + + def test_show_detail_relation_added_should_not_disclose_issue_that_is_not_visible + issue = Issue.generate!(:is_private => true) + detail = JournalDetail.new(:property => 'relation', + :prop_key => 'precedes', + :value => issue.id) + + assert_equal "Precedes Issue ##{issue.id} added", show_detail(detail, true) + assert_equal "Precedes Issue ##{issue.id} added", show_detail(detail, false) + end + + def test_show_detail_relation_deleted + detail = JournalDetail.new(:property => 'relation', + :prop_key => 'precedes', + :old_value => 1) + assert_equal "Precedes deleted (Bug #1: Can't print recipes)", show_detail(detail, true) + str = link_to("Bug #1", + "/issues/1", + :class => Issue.find(1).css_classes) + assert_equal "Precedes deleted (#{str}: #{ESCAPED_UCANT} print recipes)", + show_detail(detail, false) + end + + def test_show_detail_relation_deleted_with_inexistant_issue + inexistant_issue_number = 9999 + assert_nil Issue.find_by_id(inexistant_issue_number) + detail = JournalDetail.new(:property => 'relation', + :prop_key => 'precedes', + :old_value => inexistant_issue_number) + assert_equal "Precedes deleted (Issue #9999)", show_detail(detail, true) + assert_equal "Precedes deleted (Issue #9999)", show_detail(detail, false) + end + + def test_show_detail_relation_deleted_should_not_disclose_issue_that_is_not_visible + issue = Issue.generate!(:is_private => true) + detail = JournalDetail.new(:property => 'relation', + :prop_key => 'precedes', + :old_value => issue.id) + + assert_equal "Precedes deleted (Issue ##{issue.id})", show_detail(detail, true) + assert_equal "Precedes deleted (Issue ##{issue.id})", show_detail(detail, false) + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/projects_helper_test.rb --- a/test/unit/helpers/projects_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/projects_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -20,6 +20,7 @@ class ProjectsHelperTest < ActionView::TestCase include ApplicationHelper include ProjectsHelper + include Redmine::I18n include ERB::Util fixtures :projects, :trackers, :issue_statuses, :issues, @@ -29,8 +30,7 @@ :member_roles, :members, :groups_users, - :enabled_modules, - :workflows + :enabled_modules def setup super diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/queries_helper_test.rb --- a/test/unit/helpers/queries_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/queries_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -29,37 +29,25 @@ :projects_trackers, :custom_fields_trackers - def test_order - User.current = User.find_by_login('admin') - query = Query.new(:project => nil, :name => '_') - assert_equal 30, query.available_filters.size + def test_filters_options_has_empty_item + query = IssueQuery.new + filter_count = query.available_filters.size fo = filters_options(query) - assert_equal 31, fo.size + assert_equal filter_count + 1, fo.size assert_equal [], fo[0] - assert_equal "status_id", fo[1][1] - assert_equal "project_id", fo[2][1] - assert_equal "tracker_id", fo[3][1] - assert_equal "priority_id", fo[4][1] - assert_equal "watcher_id", fo[17][1] - assert_equal "is_private", fo[18][1] end - def test_order_custom_fields - set_language_if_valid 'en' - field = UserCustomField.new( - :name => 'order test', :field_format => 'string', - :is_for_all => true, :is_filter => true - ) - assert field.save - User.current = User.find_by_login('admin') - query = Query.new(:project => nil, :name => '_') - assert_equal 32, query.available_filters.size - fo = filters_options(query) - assert_equal 33, fo.size - assert_equal "Searchable field", fo[19][0] - assert_equal "Database", fo[20][0] - assert_equal "Project's Development status", fo[21][0] - assert_equal "Assignee's order test", fo[22][0] - assert_equal "Author's order test", fo[23][0] + def test_query_to_csv_should_translate_boolean_custom_field_values + f = IssueCustomField.generate!(:field_format => 'bool', :name => 'Boolean', :is_for_all => true, :trackers => Tracker.all) + issues = [ + Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {f.id.to_s => '0'}), + Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {f.id.to_s => '1'}) + ] + + with_locale 'fr' do + csv = query_to_csv(issues, IssueQuery.new, :columns => 'all') + assert_include "Oui", csv + assert_include "Non", csv + end end end diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/search_helper_test.rb --- a/test/unit/helpers/search_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/search_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -21,6 +21,7 @@ class SearchHelperTest < ActionView::TestCase include SearchHelper + include Redmine::I18n include ERB::Util def test_highlight_single_token diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/sort_helper_test.rb --- a/test/unit/helpers/sort_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/sort_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -19,6 +19,7 @@ class SortHelperTest < ActionView::TestCase include SortHelper + include Redmine::I18n include ERB::Util def setup @@ -30,21 +31,21 @@ sort_init 'attr1', 'desc' sort_update(['attr1', 'attr2']) - assert_equal 'attr1 DESC', sort_clause + assert_equal ['attr1 DESC'], sort_clause end def test_default_sort_clause_with_hash sort_init 'attr1', 'desc' sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'}) - assert_equal 'table1.attr1 DESC', sort_clause + assert_equal ['table1.attr1 DESC'], sort_clause end def test_default_sort_clause_with_multiple_columns sort_init 'attr1', 'desc' sort_update({'attr1' => ['table1.attr1', 'table1.attr2'], 'attr2' => 'table2.attr2'}) - assert_equal 'table1.attr1 DESC, table1.attr2 DESC', sort_clause + assert_equal ['table1.attr1 DESC', 'table1.attr2 DESC'], sort_clause end def test_params_sort @@ -53,7 +54,7 @@ sort_init 'attr1', 'desc' sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'}) - assert_equal 'table1.attr1, table2.attr2 DESC', sort_clause + assert_equal ['table1.attr1', 'table2.attr2 DESC'], sort_clause assert_equal 'attr1,attr2:desc', @session['foo_bar_sort'] end @@ -63,7 +64,7 @@ sort_init 'attr1', 'desc' sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'}) - assert_equal 'table1.attr1 DESC', sort_clause + assert_equal ['table1.attr1 DESC'], sort_clause assert_equal 'attr1:desc', @session['foo_bar_sort'] end @@ -73,7 +74,7 @@ sort_init 'attr1', 'desc' sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'}) - assert_equal 'table1.attr1, table2.attr2', sort_clause + assert_equal ['table1.attr1', 'table2.attr2'], sort_clause assert_equal 'attr1,attr2', @session['foo_bar_sort'] end diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/timelog_helper_test.rb --- a/test/unit/helpers/timelog_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/helpers/timelog_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -19,6 +19,7 @@ class TimelogHelperTest < ActionView::TestCase include TimelogHelper + include Redmine::I18n include ActionView::Helpers::TextHelper include ActionView::Helpers::DateHelper include ERB::Util diff -r d98d22a98252 -r fb9a13467253 test/unit/helpers/watchers_helper_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/helpers/watchers_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 WatchersHelperTest < ActionView::TestCase + include WatchersHelper + include Redmine::I18n + include Rails.application.routes.url_helpers + + fixtures :users, :issues + + def setup + super + set_language_if_valid('en') + User.current = nil + end + + test '#watcher_link with a non-watched object' do + expected = link_to( + "Watch", + "/watchers/watch?object_id=1&object_type=issue", + :remote => true, :method => 'post', :class => "issue-1-watcher icon icon-fav-off" + ) + assert_equal expected, watcher_link(Issue.find(1), User.find(1)) + end + + test '#watcher_link with a single objet array' do + expected = link_to( + "Watch", + "/watchers/watch?object_id=1&object_type=issue", + :remote => true, :method => 'post', :class => "issue-1-watcher icon icon-fav-off" + ) + assert_equal expected, watcher_link([Issue.find(1)], User.find(1)) + end + + test '#watcher_link with a multiple objets array' do + expected = link_to( + "Watch", + "/watchers/watch?object_id%5B%5D=1&object_id%5B%5D=3&object_type=issue", + :remote => true, :method => 'post', :class => "issue-bulk-watcher icon icon-fav-off" + ) + assert_equal expected, watcher_link([Issue.find(1), Issue.find(3)], User.find(1)) + end + + def test_watcher_link_with_nil_should_return_empty_string + assert_equal '', watcher_link(nil, User.find(1)) + end + + test '#watcher_link with a watched object' do + Watcher.create!(:watchable => Issue.find(1), :user => User.find(1)) + + expected = link_to( + "Unwatch", + "/watchers/watch?object_id=1&object_type=issue", + :remote => true, :method => 'delete', :class => "issue-1-watcher icon icon-fav" + ) + assert_equal expected, watcher_link(Issue.find(1), User.find(1)) + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/initializers/patches_test.rb --- a/test/unit/initializers/patches_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/initializers/patches_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -20,21 +20,80 @@ class PatchesTest < ActiveSupport::TestCase include Redmine::I18n - context "ActiveRecord::Base.human_attribute_name" do - setup do - Setting.default_language = 'en' + def setup + Setting.default_language = 'en' + @symbols = { :a => 1, :b => 2 } + @keys = %w( blue green red pink orange ) + @values = %w( 000099 009900 aa0000 cc0066 cc6633 ) + @hash = Hash.new + @ordered_hash = ActiveSupport::OrderedHash.new + + @keys.each_with_index do |key, index| + @hash[key] = @values[index] + @ordered_hash[key] = @values[index] + end + end + + test "ActiveRecord::Base.human_attribute_name should transform name to field_name" do + assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on') + end + + test "ActiveRecord::Base.human_attribute_name should cut extra _id suffix for better validation" do + assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on_id') + end + + test "ActiveRecord::Base.human_attribute_name should default to humanized value if no translation has been found (useful for custom fields)" do + assert_equal 'Patch name', ActiveRecord::Base.human_attribute_name('Patch name') + end + + # https://github.com/rails/rails/pull/14198/files + if RUBY_VERSION >= "1.9" + def test_indifferent_select + hash = ActiveSupport::HashWithIndifferentAccess.new(@symbols).select { |_ ,v| v == 1 } + assert_equal({ 'a' => 1 }, hash) + assert_instance_of ((Rails::VERSION::MAJOR < 4 && RUBY_VERSION < "2.1") ? + Hash : ActiveSupport::HashWithIndifferentAccess), + hash end - should "transform name to field_name" do - assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on') - end - - should "cut extra _id suffix for better validation" do - assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on_id') - end - - should "default to humanized value if no translation has been found (useful for custom fields)" do - assert_equal 'Patch name', ActiveRecord::Base.human_attribute_name('Patch name') + def test_indifferent_select_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@symbols) + indifferent_strings.select! { |_, v| v == 1 } + assert_equal({ 'a' => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end end + + def test_indifferent_reject + hash = ActiveSupport::HashWithIndifferentAccess.new(@symbols).reject { |_, v| v != 1 } + assert_equal({ 'a' => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_reject_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@symbols) + indifferent_strings.reject! { |_, v| v != 1 } + assert_equal({ 'a' => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + + if RUBY_VERSION >= "1.9" + def test_select + assert_equal @keys, @ordered_hash.select { true }.map(&:first) + new_ordered_hash = @ordered_hash.select { true } + assert_equal @keys, new_ordered_hash.map(&:first) + assert_instance_of ((Rails::VERSION::MAJOR < 4 && RUBY_VERSION < "2.1") ? + Hash : ActiveSupport::OrderedHash), + new_ordered_hash + end + end + + def test_reject + copy = @ordered_hash.dup + new_ordered_hash = @ordered_hash.reject { |k, _| k == 'pink' } + assert_equal copy, @ordered_hash + assert !new_ordered_hash.keys.include?('pink') + assert @ordered_hash.keys.include?('pink') + assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_category_test.rb --- a/test/unit/issue_category_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_category_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -26,13 +26,13 @@ def test_create assert IssueCategory.new(:project_id => 2, :name => 'New category').save - category = IssueCategory.first(:order => 'id DESC') + category = IssueCategory.order('id DESC').first assert_equal 'New category', category.name end def test_create_with_group_assignment assert IssueCategory.new(:project_id => 2, :name => 'Group assignment', :assigned_to_id => 11).save - category = IssueCategory.first(:order => 'id DESC') + category = IssueCategory.order('id DESC').first assert_kind_of Group, category.assigned_to assert_equal Group.find(11), category.assigned_to end diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_custom_field_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/issue_custom_field_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,42 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 IssueCustomFieldTest < ActiveSupport::TestCase + include Redmine::I18n + + fixtures :roles + + def test_custom_field_with_visible_set_to_false_should_validate_roles + set_language_if_valid 'en' + field = IssueCustomField.new(:name => 'Field', :field_format => 'string', :visible => false) + assert !field.save + assert_include "Roles can't be blank", field.errors.full_messages + field.role_ids = [1, 2] + assert field.save + end + + def test_changing_visible_to_true_should_clear_roles + field = IssueCustomField.create!(:name => 'Field', :field_format => 'string', :visible => false, :role_ids => [1, 2]) + assert_equal 2, field.roles.count + + field.visible = true + field.save! + assert_equal 0, field.roles.count + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_nested_set_test.rb --- a/test/unit/issue_nested_set_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_nested_set_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,33 +18,36 @@ require File.expand_path('../../test_helper', __FILE__) class IssueNestedSetTest < ActiveSupport::TestCase - fixtures :projects, :users, :members, :member_roles, :roles, + fixtures :projects, :users, :roles, :trackers, :projects_trackers, - :versions, - :issue_statuses, :issue_categories, :issue_relations, :workflows, + :issue_statuses, :issue_categories, :issue_relations, :enumerations, - :issues, - :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, - :time_entries + :issues + + def test_new_record_is_leaf + i = Issue.new + assert i.leaf? + end def test_create_root_issue + lft1 = new_issue_lft issue1 = Issue.generate! + lft2 = new_issue_lft issue2 = Issue.generate! issue1.reload issue2.reload - - assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt] - assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt] + assert_equal [issue1.id, nil, lft1, lft1 + 1], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt] + assert_equal [issue2.id, nil, lft2, lft2 + 1], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt] end def test_create_child_issue + lft = new_issue_lft parent = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent.id) + child = parent.generate_child! parent.reload child.reload - - assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt] - assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt] + assert_equal [parent.id, nil, lft, lft + 3], [parent.root_id, parent.parent_id, parent.lft, parent.rgt] + assert_equal [parent.id, parent.id, lft + 1, lft + 2], [child.root_id, child.parent_id, child.lft, child.rgt] end def test_creating_a_child_in_a_subproject_should_validate @@ -60,99 +63,95 @@ child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, :subject => 'child', :parent_issue_id => issue.id) assert !child.save - assert_not_nil child.errors[:parent_issue_id] + assert_not_equal [], child.errors[:parent_issue_id] end def test_move_a_root_to_child + lft = new_issue_lft parent1 = Issue.generate! parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - + child = parent1.generate_child! parent2.parent_issue_id = parent1.id parent2.save! child.reload parent1.reload parent2.reload - - assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt] + assert_equal [parent1.id, lft, lft + 5], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent1.id, lft + 3, lft + 4], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent1.id, lft + 1, lft + 2], [child.root_id, child.lft, child.rgt] end def test_move_a_child_to_root + lft1 = new_issue_lft parent1 = Issue.generate! + lft2 = new_issue_lft parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - + child = parent1.generate_child! child.parent_issue_id = nil child.save! child.reload parent1.reload parent2.reload - - assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent1.id, lft1, lft1 + 1], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, lft2, lft2 + 1], [parent2.root_id, parent2.lft, parent2.rgt] assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt] end def test_move_a_child_to_another_issue + lft1 = new_issue_lft parent1 = Issue.generate! + lft2 = new_issue_lft parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - + child = parent1.generate_child! child.parent_issue_id = parent2.id child.save! child.reload parent1.reload parent2.reload - - assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt] + assert_equal [parent1.id, lft1, lft1 + 1], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, lft2, lft2 + 3], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent2.id, lft2 + 1, lft2 + 2], [child.root_id, child.lft, child.rgt] end def test_move_a_child_with_descendants_to_another_issue + lft1 = new_issue_lft parent1 = Issue.generate! + lft2 = new_issue_lft parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - grandchild = Issue.generate!(:parent_issue_id => child.id) - + child = parent1.generate_child! + grandchild = child.generate_child! parent1.reload parent2.reload child.reload grandchild.reload - - assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt] - assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] - + assert_equal [parent1.id, lft1, lft1 + 5], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, lft2, lft2 + 1], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent1.id, lft1 + 1, lft1 + 4], [child.root_id, child.lft, child.rgt] + assert_equal [parent1.id, lft1 + 2, lft1 + 3], [grandchild.root_id, grandchild.lft, grandchild.rgt] child.reload.parent_issue_id = parent2.id child.save! child.reload grandchild.reload parent1.reload parent2.reload - - assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] - assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt] - assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt] - assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] + assert_equal [parent1.id, lft1, lft1 + 1], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, lft2, lft2 + 5], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent2.id, lft2 + 1, lft2 + 4], [child.root_id, child.lft, child.rgt] + assert_equal [parent2.id, lft2 + 2, lft2 + 3], [grandchild.root_id, grandchild.lft, grandchild.rgt] end def test_move_a_child_with_descendants_to_another_project + lft1 = new_issue_lft parent1 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - grandchild = Issue.generate!(:parent_issue_id => child.id) - + child = parent1.generate_child! + grandchild = child.generate_child! child.reload child.project = Project.find(2) assert child.save child.reload grandchild.reload parent1.reload - - assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [1, parent1.id, lft1, lft1 + 1], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt] assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] end @@ -160,41 +159,59 @@ def test_moving_an_issue_to_a_descendant_should_not_validate parent1 = Issue.generate! parent2 = Issue.generate! - child = Issue.generate!(:parent_issue_id => parent1.id) - grandchild = Issue.generate!(:parent_issue_id => child.id) + child = parent1.generate_child! + grandchild = child.generate_child! child.reload child.parent_issue_id = grandchild.id assert !child.save - assert_not_nil child.errors[:parent_issue_id] + assert_not_equal [], child.errors[:parent_issue_id] end - def test_moving_an_issue_should_keep_valid_relations_only - issue1 = Issue.generate! - issue2 = Issue.generate! - issue3 = Issue.generate!(:parent_issue_id => issue2.id) - issue4 = Issue.generate! - r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES) - r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES) - r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES) - issue2.reload - issue2.parent_issue_id = issue1.id - issue2.save! - assert !IssueRelation.exists?(r1.id) - assert !IssueRelation.exists?(r2.id) - assert IssueRelation.exists?(r3.id) + def test_updating_a_root_issue_should_not_trigger_update_nested_set_attributes_on_parent_change + issue = Issue.find(Issue.generate!.id) + issue.parent_issue_id = "" + issue.expects(:update_nested_set_attributes_on_parent_change).never + issue.save! + end + + def test_updating_a_child_issue_should_not_trigger_update_nested_set_attributes_on_parent_change + issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id) + issue.parent_issue_id = "1" + issue.expects(:update_nested_set_attributes_on_parent_change).never + issue.save! + end + + def test_moving_a_root_issue_should_trigger_update_nested_set_attributes_on_parent_change + issue = Issue.find(Issue.generate!.id) + issue.parent_issue_id = "1" + issue.expects(:update_nested_set_attributes_on_parent_change).once + issue.save! + end + + def test_moving_a_child_issue_to_another_parent_should_trigger_update_nested_set_attributes_on_parent_change + issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id) + issue.parent_issue_id = "2" + issue.expects(:update_nested_set_attributes_on_parent_change).once + issue.save! + end + + def test_moving_a_child_issue_to_root_should_trigger_update_nested_set_attributes_on_parent_change + issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id) + issue.parent_issue_id = "" + issue.expects(:update_nested_set_attributes_on_parent_change).once + issue.save! end def test_destroy_should_destroy_children + lft1 = new_issue_lft issue1 = Issue.generate! issue2 = Issue.generate! - issue3 = Issue.generate!(:parent_issue_id => issue2.id) - issue4 = Issue.generate!(:parent_issue_id => issue1.id) - + issue3 = issue2.generate_child! + issue4 = issue1.generate_child! issue3.init_journal(User.find(2)) issue3.subject = 'child with journal' issue3.save! - assert_difference 'Issue.count', -2 do assert_difference 'Journal.count', -1 do assert_difference 'JournalDetail.count', -1 do @@ -202,33 +219,30 @@ end end end - issue1.reload issue4.reload assert !Issue.exists?(issue2.id) assert !Issue.exists?(issue3.id) - assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt] - assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt] + assert_equal [issue1.id, lft1, lft1 + 3], [issue1.root_id, issue1.lft, issue1.rgt] + assert_equal [issue1.id, lft1 + 1, lft1 + 2], [issue4.root_id, issue4.lft, issue4.rgt] end - + def test_destroy_child_should_update_parent + lft1 = new_issue_lft issue = Issue.generate! - child1 = Issue.generate!(:parent_issue_id => issue.id) - child2 = Issue.generate!(:parent_issue_id => issue.id) - + child1 = issue.generate_child! + child2 = issue.generate_child! issue.reload - assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt] - + assert_equal [issue.id, lft1, lft1 + 5], [issue.root_id, issue.lft, issue.rgt] child2.reload.destroy - issue.reload - assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt] + assert_equal [issue.id, lft1, lft1 + 3], [issue.root_id, issue.lft, issue.rgt] end def test_destroy_parent_issue_updated_during_children_destroy parent = Issue.generate! - Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id) - Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id) + parent.generate_child!(:start_date => Date.today) + parent.generate_child!(:start_date => 2.days.from_now) assert_difference 'Issue.count', -3 do Issue.find(parent.id).destroy @@ -236,9 +250,9 @@ end def test_destroy_child_issue_with_children - root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root') - child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id) - leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id) + root = Issue.generate! + child = root.generate_child! + leaf = child.generate_child! leaf.init_journal(User.find(2)) leaf.subject = 'leaf with journal' leaf.save! @@ -256,28 +270,28 @@ end def test_destroy_issue_with_grand_child + lft1 = new_issue_lft parent = Issue.generate! - issue = Issue.generate!(:parent_issue_id => parent.id) - child = Issue.generate!(:parent_issue_id => issue.id) - grandchild1 = Issue.generate!(:parent_issue_id => child.id) - grandchild2 = Issue.generate!(:parent_issue_id => child.id) - + issue = parent.generate_child! + child = issue.generate_child! + grandchild1 = child.generate_child! + grandchild2 = child.generate_child! assert_difference 'Issue.count', -4 do Issue.find(issue.id).destroy parent.reload - assert_equal [1, 2], [parent.lft, parent.rgt] + assert_equal [lft1, lft1 + 1], [parent.lft, parent.rgt] end end def test_parent_priority_should_be_the_highest_child_priority parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal')) # Create children - child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id) + child1 = parent.generate_child!(:priority => IssuePriority.find_by_name('High')) assert_equal 'High', parent.reload.priority.name - child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id) + child2 = child1.generate_child!(:priority => IssuePriority.find_by_name('Immediate')) assert_equal 'Immediate', child1.reload.priority.name assert_equal 'Immediate', parent.reload.priority.name - child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id) + child3 = parent.generate_child!(:priority => IssuePriority.find_by_name('Low')) assert_equal 'Immediate', parent.reload.priority.name # Destroy a child child1.destroy @@ -290,9 +304,9 @@ def test_parent_dates_should_be_lowest_start_and_highest_due_dates parent = Issue.generate! - Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id) - Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id) - Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id) + parent.generate_child!(:start_date => '2010-01-25', :due_date => '2010-02-15') + parent.generate_child!( :due_date => '2010-02-13') + parent.generate_child!(:start_date => '2010-02-01', :due_date => '2010-02-22') parent.reload assert_equal Date.parse('2010-01-25'), parent.start_date assert_equal Date.parse('2010-02-22'), parent.due_date @@ -300,41 +314,72 @@ def test_parent_done_ratio_should_be_average_done_ratio_of_leaves parent = Issue.generate! - Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id) + parent.generate_child!(:done_ratio => 20) assert_equal 20, parent.reload.done_ratio - Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id) + parent.generate_child!(:done_ratio => 70) assert_equal 45, parent.reload.done_ratio - child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id) + child = parent.generate_child!(:done_ratio => 0) assert_equal 30, parent.reload.done_ratio - Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id) + child.generate_child!(:done_ratio => 30) assert_equal 30, child.reload.done_ratio assert_equal 40, parent.reload.done_ratio end def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any parent = Issue.generate! - Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id) + parent.generate_child!(:estimated_hours => 10, :done_ratio => 20) assert_equal 20, parent.reload.done_ratio - Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id) + parent.generate_child!(:estimated_hours => 20, :done_ratio => 50) assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio end + def test_parent_done_ratio_with_child_estimate_to_0_should_reach_100 + parent = Issue.generate! + issue1 = parent.generate_child! + issue2 = parent.generate_child!(:estimated_hours => 0) + assert_equal 0, parent.reload.done_ratio + issue1.reload.close! + assert_equal 50, parent.reload.done_ratio + issue2.reload.close! + assert_equal 100, parent.reload.done_ratio + end + def test_parent_estimate_should_be_sum_of_leaves parent = Issue.generate! - Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id) + parent.generate_child!(:estimated_hours => nil) assert_equal nil, parent.reload.estimated_hours - Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id) + parent.generate_child!(:estimated_hours => 5) assert_equal 5, parent.reload.estimated_hours - Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id) + parent.generate_child!(:estimated_hours => 7) assert_equal 12, parent.reload.estimated_hours end + def test_done_ratio_of_parent_with_a_child_without_estimated_time_should_not_exceed_100 + parent = Issue.generate! + parent.generate_child!(:estimated_hours => 40) + parent.generate_child!(:estimated_hours => 40) + parent.generate_child!(:estimated_hours => 20) + parent.generate_child! + parent.reload.children.each(&:close!) + assert_equal 100, parent.reload.done_ratio + end + + def test_done_ratio_of_parent_with_a_child_with_estimated_time_at_0_should_not_exceed_100 + parent = Issue.generate! + parent.generate_child!(:estimated_hours => 40) + parent.generate_child!(:estimated_hours => 40) + parent.generate_child!(:estimated_hours => 20) + parent.generate_child!(:estimated_hours => 0) + parent.reload.children.each(&:close!) + assert_equal 100, parent.reload.done_ratio + end + def test_move_parent_updates_old_parent_attributes first_parent = Issue.generate! second_parent = Issue.generate! - child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id) + child = first_parent.generate_child!(:estimated_hours => 5) assert_equal 5, first_parent.reload.estimated_hours child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id) assert_equal 7, second_parent.reload.estimated_hours @@ -343,8 +388,8 @@ def test_reschuling_a_parent_should_reschedule_subtasks parent = Issue.generate! - c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id) - c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id) + c1 = parent.generate_child!(:start_date => '2010-05-12', :due_date => '2010-05-18') + c2 = parent.generate_child!(:start_date => '2010-06-03', :due_date => '2010-06-10') parent.reload parent.reschedule_on!(Date.parse('2010-06-02')) c1.reload @@ -358,16 +403,16 @@ def test_project_copy_should_copy_issue_tree p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2]) i1 = Issue.generate!(:project => p, :subject => 'i1') - i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id) - i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id) - i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id) + i2 = i1.generate_child!(:project => p, :subject => 'i2') + i3 = i1.generate_child!(:project => p, :subject => 'i3') + i4 = i2.generate_child!(:project => p, :subject => 'i4') i5 = Issue.generate!(:project => p, :subject => 'i5') c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2]) c.copy(p, :only => 'issues') c.reload assert_equal 5, c.issues.count - ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject') + ic1, ic2, ic3, ic4, ic5 = c.issues.order('subject').all assert ic1.root? assert_equal ic1, ic2.parent assert_equal ic1, ic3.parent diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_priority_test.rb --- a/test/unit/issue_priority_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_priority_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_relation_test.rb --- a/test/unit/issue_relation_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_relation_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -28,7 +28,10 @@ :issue_relations, :enabled_modules, :enumerations, - :trackers + :trackers, + :projects_trackers + + include Redmine::I18n def test_create from = Issue.find(1) @@ -112,7 +115,40 @@ :relation_type => IssueRelation::TYPE_PRECEDES ) assert !r.save - assert_not_nil r.errors[:base] + assert_not_equal [], r.errors[:base] + end + + def test_validates_circular_dependency_of_subtask + set_language_if_valid 'en' + issue1 = Issue.generate! + issue2 = Issue.generate! + IssueRelation.create!( + :issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES + ) + child = Issue.generate!(:parent_issue_id => issue2.id) + issue1.reload + child.reload + + r = IssueRelation.new( + :issue_from => child, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_PRECEDES + ) + assert !r.save + assert_include 'This relation would create a circular dependency', r.errors.full_messages + end + + def test_subtasks_should_allow_precedes_relation + parent = Issue.generate! + child1 = Issue.generate!(:parent_issue_id => parent.id) + child2 = Issue.generate!(:parent_issue_id => parent.id) + + r = IssueRelation.new( + :issue_from => child1, :issue_to => child2, + :relation_type => IssueRelation::TYPE_PRECEDES + ) + assert r.valid? + assert r.save end def test_validates_circular_dependency_on_reverse_relations @@ -130,6 +166,50 @@ :relation_type => IssueRelation::TYPE_BLOCKED ) assert !r.save - assert_not_nil r.errors[:base] + assert_not_equal [], r.errors[:base] + end + + def test_create_should_make_journal_entry + from = Issue.find(1) + to = Issue.find(2) + from_journals = from.journals.size + to_journals = to.journals.size + relation = IssueRelation.new(:issue_from => from, :issue_to => to, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert relation.save + from.reload + to.reload + relation.reload + assert_equal from.journals.size, (from_journals + 1) + assert_equal to.journals.size, (to_journals + 1) + assert_equal 'relation', from.journals.last.details.last.property + assert_equal 'precedes', from.journals.last.details.last.prop_key + assert_equal '2', from.journals.last.details.last.value + assert_nil from.journals.last.details.last.old_value + assert_equal 'relation', to.journals.last.details.last.property + assert_equal 'follows', to.journals.last.details.last.prop_key + assert_equal '1', to.journals.last.details.last.value + assert_nil to.journals.last.details.last.old_value + end + + def test_delete_should_make_journal_entry + relation = IssueRelation.find(1) + from = relation.issue_from + to = relation.issue_to + from_journals = from.journals.size + to_journals = to.journals.size + assert relation.destroy + from.reload + to.reload + assert_equal from.journals.size, (from_journals + 1) + assert_equal to.journals.size, (to_journals + 1) + assert_equal 'relation', from.journals.last.details.last.property + assert_equal 'blocks', from.journals.last.details.last.prop_key + assert_equal '9', from.journals.last.details.last.old_value + assert_nil from.journals.last.details.last.value + assert_equal 'relation', to.journals.last.details.last.property + assert_equal 'blocked', to.journals.last.details.last.prop_key + assert_equal '10', to.journals.last.details.last.old_value + assert_nil to.journals.last.details.last.value end end diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_status_test.rb --- a/test/unit/issue_status_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_status_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -36,8 +36,8 @@ assert_difference 'IssueStatus.count', -1 do assert status.destroy end - assert_nil WorkflowTransition.first(:conditions => {:old_status_id => status.id}) - assert_nil WorkflowTransition.first(:conditions => {:new_status_id => status.id}) + assert_nil WorkflowTransition.where(:old_status_id => status.id).first + assert_nil WorkflowTransition.where(:new_status_id => status.id).first end def test_destroy_status_in_use @@ -98,16 +98,15 @@ with_settings :issue_done_ratio => 'issue_field' do IssueStatus.update_issue_done_ratios - assert_equal 0, Issue.count(:conditions => {:done_ratio => 50}) + assert_equal 0, Issue.where(:done_ratio => 50).count end end def test_update_done_ratios_with_issue_done_ratio_set_to_issue_status_should_update_issues IssueStatus.find(1).update_attribute(:default_done_ratio, 50) - with_settings :issue_done_ratio => 'issue_status' do IssueStatus.update_issue_done_ratios - issues = Issue.all(:conditions => {:status_id => 1}) + issues = Issue.where(:status_id => 1) assert_equal [50], issues.map {|issue| issue.read_attribute(:done_ratio)}.uniq end end diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_test.rb --- a/test/unit/issue_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -35,6 +35,19 @@ User.current = nil end + def test_initialize + issue = Issue.new + + assert_nil issue.project_id + assert_nil issue.tracker_id + assert_nil issue.author_id + assert_nil issue.assigned_to_id + assert_nil issue.category_id + + assert_equal IssueStatus.default, issue.status + assert_equal IssuePriority.default, issue.priority + end + def test_create issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, @@ -79,6 +92,36 @@ assert_include 'Due date must be greater than start date', issue.errors.full_messages end + def test_start_date_lesser_than_soonest_start_should_not_validate_on_create + issue = Issue.generate(:start_date => '2013-06-04') + issue.stubs(:soonest_start).returns(Date.parse('2013-06-10')) + assert !issue.valid? + assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages + end + + def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed + issue = Issue.generate!(:start_date => '2013-06-04') + issue.stubs(:soonest_start).returns(Date.parse('2013-06-10')) + issue.start_date = '2013-06-07' + assert !issue.valid? + assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages + end + + def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged + issue = Issue.generate!(:start_date => '2013-06-04') + issue.stubs(:soonest_start).returns(Date.parse('2013-06-10')) + assert issue.valid? + end + + def test_estimated_hours_should_be_validated + set_language_if_valid 'en' + ['-2'].each do |invalid| + issue = Issue.new(:estimated_hours => invalid) + assert !issue.valid? + assert_include 'Estimated time is invalid', issue.errors.full_messages + end + end + def test_create_with_required_custom_field set_language_if_valid 'en' field = IssueCustomField.find_by_name('Database') @@ -111,7 +154,7 @@ assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, :subject => 'Group assignment', :assigned_to_id => 11).save - issue = Issue.first(:order => 'id DESC') + issue = Issue.order('id DESC').first assert_kind_of Group, issue.assigned_to assert_equal Group.find(11), issue.assigned_to end @@ -224,7 +267,7 @@ def test_visible_scope_for_member user = User.find(9) - # User should see issues of projects for which he has view_issues permissions only + # User should see issues of projects for which user has view_issues permissions only Role.non_member.remove_permission!(:view_issues) Member.create!(:principal => user, :project_id => 3, :role_ids => [2]) issues = Issue.visible(user).all @@ -239,18 +282,18 @@ assert user.groups.any? Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2]) Role.non_member.remove_permission!(:view_issues) - + issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :assigned_to => user.groups.first, :is_private => true) - + Role.find(2).update_attribute :issues_visibility, 'default' issues = Issue.visible(User.find(8)).all assert issues.any? assert issues.include?(issue) - + Role.find(2).update_attribute :issues_visibility, 'own' issues = Issue.visible(User.find(8)).all assert issues.any? @@ -263,7 +306,7 @@ assert user.projects.empty? issues = Issue.visible(user).all assert issues.any? - # Admin should see issues on private projects that he does not belong to + # Admin should see issues on private projects that admin does not belong to assert issues.detect {|issue| !issue.project.is_public?} # Admin should see private issues of other users assert issues.detect {|issue| issue.is_private? && issue.author != user} @@ -287,7 +330,23 @@ end def test_visible_and_nested_set_scopes - assert_equal 0, Issue.find(1).descendants.visible.all.size + user = User.generate! + parent = Issue.generate!(:assigned_to => user) + assert parent.visible?(user) + child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user) + child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user) + parent.reload + child1.reload + child2.reload + assert child1.visible?(user) + assert child2.visible?(user) + assert_equal 2, parent.descendants.count + assert_equal 2, parent.descendants.visible(user).count + # awesome_nested_set 2-1-stable branch has regression. + # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6 + # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft: + assert_equal 2, parent.descendants.collect{|i| i}.size + assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size end def test_open_scope @@ -300,6 +359,16 @@ assert_equal issues, issues.select(&:closed?) end + def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues + version = Version.find(2) + assert version.fixed_issues.any? + assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort + end + + def test_fixed_version_scope_with_empty_array_should_return_no_result + assert_equal 0, Issue.fixed_version([]).count + end + def test_errors_full_messages_should_include_custom_fields_errors field = IssueCustomField.find_by_name('Database') @@ -402,6 +471,21 @@ assert_equal 'MySQL', issue.custom_field_value(1) end + def test_reload_should_reload_custom_field_values + issue = Issue.generate! + issue.custom_field_values = {'2' => 'Foo'} + issue.save! + + issue = Issue.order('id desc').first + assert_equal 'Foo', issue.custom_field_value(2) + + issue.custom_field_values = {'2' => 'Bar'} + assert_equal 'Bar', issue.custom_field_value(2) + + issue.reload + assert_equal 'Foo', issue.custom_field_value(2) + end + def test_should_update_issue_with_disabled_tracker p = Project.find(1) issue = Issue.find(1) @@ -422,7 +506,7 @@ issue.tracker_id = 2 issue.subject = 'New subject' assert !issue.save - assert_not_nil issue.errors[:tracker_id] + assert_not_equal [], issue.errors[:tracker_id] end def test_category_based_assignment @@ -441,9 +525,9 @@ WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false) - WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, - :new_status_id => 4, :author => false, - :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 4, + :author => false, :assignee => true) WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true) @@ -469,15 +553,35 @@ :project_id => 1, :author => user, :assigned_to => user) assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id) + + group = Group.generate! + group.users << user + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author => user, + :assigned_to => group) + assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id) + end + + def test_new_statuses_allowed_to_should_consider_group_assignment + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 4, + :author => false, :assignee => true) + user = User.find(2) + group = Group.generate! + group.users << user + + issue = Issue.generate!(:author_id => 1, :assigned_to => group) + assert_include 4, issue.new_statuses_allowed_to(user).map(&:id) end def test_new_statuses_allowed_to_should_return_all_transitions_for_admin admin = User.find(1) issue = Issue.find(1) assert !admin.member_of?(issue.project) - expected_statuses = [issue.status] + - WorkflowTransition.find_all_by_old_status_id( - issue.status_id).map(&:new_status).uniq.sort + expected_statuses = [issue.status] + + WorkflowTransition.where(:old_status_id => issue.status_id). + map(&:new_status).uniq.sort assert_equal expected_statuses, issue.new_statuses_allowed_to(admin) end @@ -602,6 +706,16 @@ assert values.detect {|value| value.custom_field == cf2} end + def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only + enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2]) + disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2]) + user = User.find(2) + issue = Issue.new(:project_id => 1, :tracker_id => 1) + + assert_include enabled_cf, issue.editable_custom_fields(user) + assert_not_include disabled_cf, issue.editable_custom_fields(user) + end + def test_safe_attributes_should_accept_target_tracker_writable_fields WorkflowPermission.delete_all WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, @@ -802,6 +916,49 @@ assert_equal copy.author, child_copy.author end + def test_copy_as_a_child_of_copied_issue_should_not_copy_itself + parent = Issue.generate! + child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1') + child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2') + + copy = parent.reload.copy + copy.parent_issue_id = parent.id + copy.author = User.find(7) + assert_difference 'Issue.count', 3 do + assert copy.save + end + parent.reload + copy.reload + assert_equal parent, copy.parent + assert_equal 3, parent.children.count + assert_equal 5, parent.descendants.count + assert_equal 2, copy.children.count + assert_equal 2, copy.descendants.count + end + + def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself + parent = Issue.generate! + child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1') + child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2') + + copy = parent.reload.copy + copy.parent_issue_id = child1.id + copy.author = User.find(7) + assert_difference 'Issue.count', 3 do + assert copy.save + end + parent.reload + child1.reload + copy.reload + assert_equal child1, copy.parent + assert_equal 2, parent.children.count + assert_equal 5, parent.descendants.count + assert_equal 1, child1.children.count + assert_equal 3, child1.descendants.count + assert_equal 2, copy.children.count + assert_equal 2, copy.descendants.count + end + def test_copy_should_copy_subtasks_to_target_project issue = Issue.generate_with_descendants! @@ -876,8 +1033,8 @@ assert issue1.reload.duplicates.include?(issue2) # Closing issue 1 - issue1.init_journal(User.find(:first), "Closing issue1") - issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true} + issue1.init_journal(User.first, "Closing issue1") + issue1.status = IssueStatus.where(:is_closed => true).first assert issue1.save # 2 and 3 should be also closed assert issue2.reload.closed? @@ -895,8 +1052,8 @@ assert !issue2.reload.duplicates.include?(issue1) # Closing issue 2 - issue2.init_journal(User.find(:first), "Closing issue2") - issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true} + issue2.init_journal(User.first, "Closing issue2") + issue2.status = IssueStatus.where(:is_closed => true).first assert issue2.save # 1 should not be also closed assert !issue1.reload.closed? @@ -914,7 +1071,7 @@ :status_id => 1, :fixed_version_id => 1, :subject => 'New issue') assert !issue.save - assert_not_nil issue.errors[:fixed_version_id] + assert_not_equal [], issue.errors[:fixed_version_id] end def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version @@ -922,7 +1079,7 @@ :status_id => 1, :fixed_version_id => 2, :subject => 'New issue') assert !issue.save - assert_not_nil issue.errors[:fixed_version_id] + assert_not_equal [], issue.errors[:fixed_version_id] end def test_should_be_able_to_assign_a_new_issue_to_an_open_version @@ -943,7 +1100,7 @@ issue = Issue.find(11) issue.status_id = 1 assert !issue.save - assert_not_nil issue.errors[:base] + assert_not_equal [], issue.errors[:base] end def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version @@ -972,7 +1129,7 @@ def test_should_keep_shared_version_when_changing_project Version.find(2).update_attribute :sharing, 'tree' - + issue = Issue.find(2) assert_equal 2, issue.fixed_version_id issue.project_id = 3 @@ -1110,57 +1267,51 @@ assert_nil copy.custom_value_for(2) end - context "#copy" do - setup do - @issue = Issue.find(1) - end + test "#copy should not create a journal" do + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3) + copy.save! + assert_equal 0, copy.reload.journals.size + end - should "not create a journal" do - copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3) - copy.save! - assert_equal 0, copy.reload.journals.size - end + test "#copy should allow assigned_to changes" do + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3) + assert_equal 3, copy.assigned_to_id + end - should "allow assigned_to changes" do - copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3) - assert_equal 3, copy.assigned_to_id - end + test "#copy should allow status changes" do + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2) + assert_equal 2, copy.status_id + end - should "allow status changes" do - copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2) - assert_equal 2, copy.status_id - end + test "#copy should allow start date changes" do + date = Date.today + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date) + assert_equal date, copy.start_date + end - should "allow start date changes" do - date = Date.today - copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date) - assert_equal date, copy.start_date - end + test "#copy should allow due date changes" do + date = Date.today + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date) + assert_equal date, copy.due_date + end - should "allow due date changes" do - date = Date.today - copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date) - assert_equal date, copy.due_date - end + test "#copy should set current user as author" do + User.current = User.find(9) + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2) + assert_equal User.current, copy.author + end - should "set current user as author" do - User.current = User.find(9) - copy = @issue.copy(:project_id => 3, :tracker_id => 2) - assert_equal User.current, copy.author - end + test "#copy should create a journal with notes" do + date = Date.today + notes = "Notes added when copying" + copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date) + copy.init_journal(User.current, notes) + copy.save! - should "create a journal with notes" do - date = Date.today - notes = "Notes added when copying" - copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date) - copy.init_journal(User.current, notes) - copy.save! - - assert_equal 1, copy.journals.size - journal = copy.journals.first - assert_equal 0, journal.details.size - assert_equal notes, journal.notes - end + assert_equal 1, copy.journals.size + journal = copy.journals.first + assert_equal 0, journal.details.size + assert_equal notes, journal.notes end def test_valid_parent_project @@ -1250,6 +1401,15 @@ assert_nil TimeEntry.find_by_issue_id(1) end + def test_destroy_should_delete_time_entries_custom_values + issue = Issue.generate! + time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'}) + + assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do + assert issue.destroy + end + end + def test_destroying_a_deleted_issue_should_not_raise_an_error issue = Issue.find(1) Issue.find(1).destroy @@ -1370,6 +1530,7 @@ :relation_type => IssueRelation::TYPE_PRECEDES) assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + issue1.reload issue1.due_date = '2012-10-23' issue1.save! issue2.reload @@ -1384,6 +1545,7 @@ :relation_type => IssueRelation::TYPE_PRECEDES) assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + issue1.reload issue1.start_date = '2012-09-17' issue1.due_date = '2012-09-18' issue1.save! @@ -1402,6 +1564,7 @@ :relation_type => IssueRelation::TYPE_PRECEDES) assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + issue1.reload issue1.start_date = '2012-09-17' issue1.due_date = '2012-09-18' issue1.save! @@ -1425,99 +1588,139 @@ end end + def test_child_issue_should_consider_parent_soonest_start_on_create + set_language_if_valid 'en' + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + issue1.reload + issue2.reload + assert_equal Date.parse('2012-10-18'), issue2.start_date + + child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16', + :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1) + assert !child.valid? + assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages + assert_equal Date.parse('2012-10-18'), child.soonest_start + child.start_date = '2012-10-18' + assert child.save + end + + def test_setting_parent_to_a_dependent_issue_should_not_validate + set_language_if_valid 'en' + issue1 = Issue.generate! + issue2 = Issue.generate! + issue3 = Issue.generate! + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES) + IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES) + issue3.reload + issue3.parent_issue_id = issue2.id + assert !issue3.valid? + assert_include 'Parent task is invalid', issue3.errors.full_messages + end + + def test_setting_parent_should_not_allow_circular_dependency + set_language_if_valid 'en' + issue1 = Issue.generate! + issue2 = Issue.generate! + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES) + issue3 = Issue.generate! + issue2.reload + issue2.parent_issue_id = issue3.id + issue2.save! + issue4 = Issue.generate! + IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES) + issue4.reload + issue4.parent_issue_id = issue1.id + assert !issue4.valid? + assert_include 'Parent task is invalid', issue4.errors.full_messages + end + def test_overdue assert Issue.new(:due_date => 1.day.ago.to_date).overdue? assert !Issue.new(:due_date => Date.today).overdue? assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue? assert !Issue.new(:due_date => nil).overdue? assert !Issue.new(:due_date => 1.day.ago.to_date, - :status => IssueStatus.find(:first, - :conditions => {:is_closed => true}) + :status => IssueStatus.where(:is_closed => true).first ).overdue? end - context "#behind_schedule?" do - should "be false if the issue has no start_date" do - assert !Issue.new(:start_date => nil, - :due_date => 1.day.from_now.to_date, - :done_ratio => 0).behind_schedule? - end + test "#behind_schedule? should be false if the issue has no start_date" do + assert !Issue.new(:start_date => nil, + :due_date => 1.day.from_now.to_date, + :done_ratio => 0).behind_schedule? + end - should "be false if the issue has no end_date" do - assert !Issue.new(:start_date => 1.day.from_now.to_date, - :due_date => nil, - :done_ratio => 0).behind_schedule? - end + test "#behind_schedule? should be false if the issue has no end_date" do + assert !Issue.new(:start_date => 1.day.from_now.to_date, + :due_date => nil, + :done_ratio => 0).behind_schedule? + end - should "be false if the issue has more done than it's calendar time" do - assert !Issue.new(:start_date => 50.days.ago.to_date, - :due_date => 50.days.from_now.to_date, - :done_ratio => 90).behind_schedule? - end + test "#behind_schedule? should be false if the issue has more done than it's calendar time" do + assert !Issue.new(:start_date => 50.days.ago.to_date, + :due_date => 50.days.from_now.to_date, + :done_ratio => 90).behind_schedule? + end - should "be true if the issue hasn't been started at all" do - assert Issue.new(:start_date => 1.day.ago.to_date, - :due_date => 1.day.from_now.to_date, - :done_ratio => 0).behind_schedule? - end + test "#behind_schedule? should be true if the issue hasn't been started at all" do + assert Issue.new(:start_date => 1.day.ago.to_date, + :due_date => 1.day.from_now.to_date, + :done_ratio => 0).behind_schedule? + end - should "be true if the issue has used more calendar time than it's done ratio" do - assert Issue.new(:start_date => 100.days.ago.to_date, - :due_date => Date.today, - :done_ratio => 90).behind_schedule? + test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do + assert Issue.new(:start_date => 100.days.ago.to_date, + :due_date => Date.today, + :done_ratio => 90).behind_schedule? + end + + test "#assignable_users should be Users" do + assert_kind_of User, Issue.find(1).assignable_users.first + end + + test "#assignable_users should include the issue author" do + non_project_member = User.generate! + issue = Issue.generate!(:author => non_project_member) + + assert issue.assignable_users.include?(non_project_member) + end + + test "#assignable_users should include the current assignee" do + user = User.generate! + issue = Issue.generate!(:assigned_to => user) + user.lock! + + assert Issue.find(issue.id).assignable_users.include?(user) + end + + test "#assignable_users should not show the issue author twice" do + assignable_user_ids = Issue.find(1).assignable_users.collect(&:id) + assert_equal 2, assignable_user_ids.length + + assignable_user_ids.each do |user_id| + assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, + "User #{user_id} appears more or less than once" end end - context "#assignable_users" do - should "be Users" do - assert_kind_of User, Issue.find(1).assignable_users.first + test "#assignable_users with issue_group_assignment should include groups" do + issue = Issue.new(:project => Project.find(2)) + + with_settings :issue_group_assignment => '1' do + assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort + assert issue.assignable_users.include?(Group.find(11)) end + end - should "include the issue author" do - non_project_member = User.generate! - issue = Issue.generate!(:author => non_project_member) + test "#assignable_users without issue_group_assignment should not include groups" do + issue = Issue.new(:project => Project.find(2)) - assert issue.assignable_users.include?(non_project_member) - end - - should "include the current assignee" do - user = User.generate! - issue = Issue.generate!(:assigned_to => user) - user.lock! - - assert Issue.find(issue.id).assignable_users.include?(user) - end - - should "not show the issue author twice" do - assignable_user_ids = Issue.find(1).assignable_users.collect(&:id) - assert_equal 2, assignable_user_ids.length - - assignable_user_ids.each do |user_id| - assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, - "User #{user_id} appears more or less than once" - end - end - - context "with issue_group_assignment" do - should "include groups" do - issue = Issue.new(:project => Project.find(2)) - - with_settings :issue_group_assignment => '1' do - assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort - assert issue.assignable_users.include?(Group.find(11)) - end - end - end - - context "without issue_group_assignment" do - should "not include groups" do - issue = Issue.new(:project => Project.find(2)) - - with_settings :issue_group_assignment => '0' do - assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort - assert !issue.assignable_users.include?(Group.find(11)) - end - end + with_settings :issue_group_assignment => '0' do + assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort + assert !issue.assignable_users.include?(Group.find(11)) end end @@ -1527,9 +1730,47 @@ :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30') + with_settings :notified_events => %w(issue_added) do + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + end - assert issue.save - assert_equal 1, ActionMailer::Base.deliveries.size + def test_create_should_send_one_email_notification_with_both_settings + ActionMailer::Base.deliveries.clear + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 3, :status_id => 1, + :priority => IssuePriority.all.first, + :subject => 'test_create', :estimated_hours => '1:30') + with_settings :notified_events => %w(issue_added issue_updated) do + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + end + + def test_create_should_not_send_email_notification_with_no_setting + ActionMailer::Base.deliveries.clear + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 3, :status_id => 1, + :priority => IssuePriority.all.first, + :subject => 'test_create', :estimated_hours => '1:30') + with_settings :notified_events => [] do + assert issue.save + assert_equal 0, ActionMailer::Base.deliveries.size + end + end + + def test_update_should_notify_previous_assignee + ActionMailer::Base.deliveries.clear + user = User.find(3) + user.members.update_all ["mail_notification = ?", false] + user.update_attribute :mail_notification, 'only_assigned' + + issue = Issue.find(2) + issue.init_journal User.find(1) + issue.assigned_to = nil + issue.save! + assert_include user.mail, ActionMailer::Base.deliveries.last.bcc end def test_stale_issue_should_not_send_email_notification @@ -1539,16 +1780,18 @@ issue.init_journal(User.find(1)) issue.subject = 'Subjet update' - assert issue.save - assert_equal 1, ActionMailer::Base.deliveries.size - ActionMailer::Base.deliveries.clear + with_settings :notified_events => %w(issue_updated) do + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + ActionMailer::Base.deliveries.clear - stale.init_journal(User.find(1)) - stale.subject = 'Another subjet update' - assert_raise ActiveRecord::StaleObjectError do - stale.save + stale.init_journal(User.find(1)) + stale.subject = 'Another subjet update' + assert_raise ActiveRecord::StaleObjectError do + stale.save + end + assert ActionMailer::Base.deliveries.empty? end - assert ActionMailer::Base.deliveries.empty? end def test_journalized_description @@ -1566,7 +1809,7 @@ end end - detail = JournalDetail.first(:order => 'id DESC') + detail = JournalDetail.order('id DESC').first assert_equal i, detail.journal.journalized assert_equal 'attr', detail.property assert_equal 'description', detail.prop_key @@ -1576,7 +1819,7 @@ def test_blank_descriptions_should_not_be_journalized IssueCustomField.delete_all - Issue.update_all("description = NULL", "id=1") + Issue.where(:id => 1).update_all("description = NULL") i = Issue.find(1) i.init_journal(User.find(2)) @@ -1630,7 +1873,7 @@ end def test_saving_twice_should_not_duplicate_journal_details - i = Issue.find(:first) + i = Issue.first i.init_journal(User.find(2), 'Some notes') # initial changes i.subject = 'New subject' @@ -1639,7 +1882,7 @@ assert i.save end # 1 more change - i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id]) + i.priority = IssuePriority.where("id <> ?", i.priority_id).first assert_no_difference 'Journal.count' do assert_difference 'JournalDetail.count', 1 do i.save @@ -1668,6 +1911,136 @@ assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort end + def test_all_dependent_issues_with_subtask + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue = Issue.generate!(:project => project) + childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + + assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_does_not_include_self + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue = Issue.generate!(:project => project) + childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + + assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id) + end + + def test_all_dependent_issues_with_parenttask_and_sibling + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue = Issue.generate!(:project => project) + childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + + assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id) + end + + def test_all_dependent_issues_with_relation_to_leaf_in_other_tree + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => childIssue2_2, + :relation_type => IssueRelation::TYPE_BLOCKS) + + assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_with_relation_to_parent_in_other_tree + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => parentIssue2, + :relation_type => IssueRelation::TYPE_BLOCKS) + + assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_with_transitive_relation + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + independentIssue = Issue.generate!(:project => project) + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => childIssue2_1, + :relation_type => IssueRelation::TYPE_RELATES) + + assert IssueRelation.create(:issue_from => childIssue2_1, + :issue_to => independentIssue, + :relation_type => IssueRelation::TYPE_RELATES) + + assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_with_transitive_relation2 + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + independentIssue = Issue.generate!(:project => project) + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => independentIssue, + :relation_type => IssueRelation::TYPE_RELATES) + + assert IssueRelation.create(:issue_from => independentIssue, + :issue_to => childIssue2_1, + :relation_type => IssueRelation::TYPE_RELATES) + + assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + + end + def test_all_dependent_issues_with_persistent_circular_dependency IssueRelation.delete_all assert IssueRelation.create!(:issue_from => Issue.find(1), @@ -1680,8 +2053,8 @@ r = IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(7), :relation_type => IssueRelation::TYPE_PRECEDES) - IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id]) - + IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1") + assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort end @@ -1700,131 +2073,99 @@ r = IssueRelation.create!(:issue_from => Issue.find(8), :issue_to => Issue.find(7), :relation_type => IssueRelation::TYPE_RELATES) - IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id]) - + IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 2") + r = IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(7), :relation_type => IssueRelation::TYPE_RELATES) - IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id]) + IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1") assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort end - context "#done_ratio" do - setup do - @issue = Issue.find(1) - @issue_status = IssueStatus.find(1) - @issue_status.update_attribute(:default_done_ratio, 50) - @issue2 = Issue.find(2) - @issue_status2 = IssueStatus.find(2) - @issue_status2.update_attribute(:default_done_ratio, 0) + test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do + @issue = Issue.find(1) + @issue_status = IssueStatus.find(1) + @issue_status.update_attribute(:default_done_ratio, 50) + @issue2 = Issue.find(2) + @issue_status2 = IssueStatus.find(2) + @issue_status2.update_attribute(:default_done_ratio, 0) + + with_settings :issue_done_ratio => 'issue_field' do + assert_equal 0, @issue.done_ratio + assert_equal 30, @issue2.done_ratio end - teardown do - Setting.issue_done_ratio = 'issue_field' - end - - context "with Setting.issue_done_ratio using the issue_field" do - setup do - Setting.issue_done_ratio = 'issue_field' - end - - should "read the issue's field" do - assert_equal 0, @issue.done_ratio - assert_equal 30, @issue2.done_ratio - end - end - - context "with Setting.issue_done_ratio using the issue_status" do - setup do - Setting.issue_done_ratio = 'issue_status' - end - - should "read the Issue Status's default done ratio" do - assert_equal 50, @issue.done_ratio - assert_equal 0, @issue2.done_ratio - end + with_settings :issue_done_ratio => 'issue_status' do + assert_equal 50, @issue.done_ratio + assert_equal 0, @issue2.done_ratio end end - context "#update_done_ratio_from_issue_status" do - setup do - @issue = Issue.find(1) - @issue_status = IssueStatus.find(1) - @issue_status.update_attribute(:default_done_ratio, 50) - @issue2 = Issue.find(2) - @issue_status2 = IssueStatus.find(2) - @issue_status2.update_attribute(:default_done_ratio, 0) + test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do + @issue = Issue.find(1) + @issue_status = IssueStatus.find(1) + @issue_status.update_attribute(:default_done_ratio, 50) + @issue2 = Issue.find(2) + @issue_status2 = IssueStatus.find(2) + @issue_status2.update_attribute(:default_done_ratio, 0) + + with_settings :issue_done_ratio => 'issue_field' do + @issue.update_done_ratio_from_issue_status + @issue2.update_done_ratio_from_issue_status + + assert_equal 0, @issue.read_attribute(:done_ratio) + assert_equal 30, @issue2.read_attribute(:done_ratio) end - context "with Setting.issue_done_ratio using the issue_field" do - setup do - Setting.issue_done_ratio = 'issue_field' - end + with_settings :issue_done_ratio => 'issue_status' do + @issue.update_done_ratio_from_issue_status + @issue2.update_done_ratio_from_issue_status - should "not change the issue" do - @issue.update_done_ratio_from_issue_status - @issue2.update_done_ratio_from_issue_status - - assert_equal 0, @issue.read_attribute(:done_ratio) - assert_equal 30, @issue2.read_attribute(:done_ratio) - end - end - - context "with Setting.issue_done_ratio using the issue_status" do - setup do - Setting.issue_done_ratio = 'issue_status' - end - - should "change the issue's done ratio" do - @issue.update_done_ratio_from_issue_status - @issue2.update_done_ratio_from_issue_status - - assert_equal 50, @issue.read_attribute(:done_ratio) - assert_equal 0, @issue2.read_attribute(:done_ratio) - end + assert_equal 50, @issue.read_attribute(:done_ratio) + assert_equal 0, @issue2.read_attribute(:done_ratio) end end test "#by_tracker" do User.current = User.anonymous groups = Issue.by_tracker(Project.find(1)) - assert_equal 3, groups.size + assert_equal 3, groups.count assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} end test "#by_version" do User.current = User.anonymous groups = Issue.by_version(Project.find(1)) - assert_equal 3, groups.size + assert_equal 3, groups.count assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i} end test "#by_priority" do User.current = User.anonymous groups = Issue.by_priority(Project.find(1)) - assert_equal 4, groups.size + assert_equal 4, groups.count assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} end test "#by_category" do User.current = User.anonymous groups = Issue.by_category(Project.find(1)) - assert_equal 2, groups.size + assert_equal 2, groups.count assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i} end test "#by_assigned_to" do User.current = User.anonymous groups = Issue.by_assigned_to(Project.find(1)) - assert_equal 2, groups.size + assert_equal 2, groups.count assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i} end test "#by_author" do User.current = User.anonymous groups = Issue.by_author(Project.find(1)) - assert_equal 4, groups.size + assert_equal 4, groups.count assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} end @@ -1832,7 +2173,7 @@ User.current = User.anonymous groups = Issue.by_subproject(Project.find(1)) # Private descendant not visible - assert_equal 1, groups.size + assert_equal 1, groups.count assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i} end @@ -1855,48 +2196,42 @@ assert_equal before, Issue.on_active_project.length end - context "Issue#recipients" do - setup do - @project = Project.find(1) - @author = User.generate! - @assignee = User.generate! - @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author) + test "Issue#recipients should include project recipients" do + issue = Issue.generate! + assert issue.project.recipients.present? + issue.project.recipients.each do |project_recipient| + assert issue.recipients.include?(project_recipient) end + end - should "include project recipients" do - assert @project.recipients.present? - @project.recipients.each do |project_recipient| - assert @issue.recipients.include?(project_recipient) - end - end + test "Issue#recipients should include the author if the author is active" do + issue = Issue.generate!(:author => User.generate!) + assert issue.author, "No author set for Issue" + assert issue.recipients.include?(issue.author.mail) + end - should "include the author if the author is active" do - assert @issue.author, "No author set for Issue" - assert @issue.recipients.include?(@issue.author.mail) - end + test "Issue#recipients should include the assigned to user if the assigned to user is active" do + issue = Issue.generate!(:assigned_to => User.generate!) + assert issue.assigned_to, "No assigned_to set for Issue" + assert issue.recipients.include?(issue.assigned_to.mail) + end - should "include the assigned to user if the assigned to user is active" do - assert @issue.assigned_to, "No assigned_to set for Issue" - assert @issue.recipients.include?(@issue.assigned_to.mail) - end + test "Issue#recipients should not include users who opt out of all email" do + issue = Issue.generate!(:author => User.generate!) + issue.author.update_attribute(:mail_notification, :none) + assert !issue.recipients.include?(issue.author.mail) + end - should "not include users who opt out of all email" do - @author.update_attribute(:mail_notification, :none) + test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do + issue = Issue.generate!(:author => User.generate!) + issue.author.update_attribute(:mail_notification, :only_assigned) + assert !issue.recipients.include?(issue.author.mail) + end - assert !@issue.recipients.include?(@issue.author.mail) - end - - should "not include the issue author if they are only notified of assigned issues" do - @author.update_attribute(:mail_notification, :only_assigned) - - assert !@issue.recipients.include?(@issue.author.mail) - end - - should "not include the assigned user if they are only notified of owned issues" do - @assignee.update_attribute(:mail_notification, :only_owner) - - assert !@issue.recipients.include?(@issue.assigned_to.mail) - end + test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do + issue = Issue.generate!(:assigned_to => User.generate!) + issue.assigned_to.update_attribute(:mail_notification, :only_owner) + assert !issue.recipients.include?(issue.assigned_to.mail) end def test_last_journal_id_with_journals_should_return_the_journal_id @@ -1916,6 +2251,12 @@ assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('') end + def test_css_classes_should_include_tracker + issue = Issue.new(:tracker => Tracker.find(2)) + classes = issue.css_classes.split(' ') + assert_include 'tracker-2', classes + end + def test_css_classes_should_include_priority issue = Issue.new(:priority => IssuePriority.find(8)) classes = issue.css_classes.split(' ') @@ -1923,6 +2264,21 @@ assert_include 'priority-highest', classes end + def test_css_classes_should_include_user_and_group_assignment + project = Project.first + user = User.generate! + group = Group.generate! + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.users << user + assert user.member_of?(project) + issue1 = Issue.generate(:assigned_to_id => group.id) + assert_include 'assigned-to-my-group', issue1.css_classes(user) + assert_not_include 'assigned-to-me', issue1.css_classes(user) + issue2 = Issue.generate(:assigned_to_id => user.id) + assert_not_include 'assigned-to-my-group', issue2.css_classes(user) + assert_include 'assigned-to-me', issue2.css_classes(user) + end + def test_save_attachments_with_hash_should_save_attachments_in_keys_order set_tmp_attachments_directory issue = Issue.generate! @@ -1936,4 +2292,77 @@ assert_equal 3, issue.reload.attachments.count assert_equal %w(upload foo bar), issue.attachments.map(&:filename) end + + def test_closed_on_should_be_nil_when_creating_an_open_issue + issue = Issue.generate!(:status_id => 1).reload + assert !issue.closed? + assert_nil issue.closed_on + end + + def test_closed_on_should_be_set_when_creating_a_closed_issue + issue = Issue.generate!(:status_id => 5).reload + assert issue.closed? + assert_not_nil issue.closed_on + assert_equal issue.updated_on, issue.closed_on + assert_equal issue.created_on, issue.closed_on + end + + def test_closed_on_should_be_nil_when_updating_an_open_issue + issue = Issue.find(1) + issue.subject = 'Not closed yet' + issue.save! + issue.reload + assert_nil issue.closed_on + end + + def test_closed_on_should_be_set_when_closing_an_open_issue + issue = Issue.find(1) + issue.subject = 'Now closed' + issue.status_id = 5 + issue.save! + issue.reload + assert_not_nil issue.closed_on + assert_equal issue.updated_on, issue.closed_on + end + + def test_closed_on_should_not_be_updated_when_updating_a_closed_issue + issue = Issue.open(false).first + was_closed_on = issue.closed_on + assert_not_nil was_closed_on + issue.subject = 'Updating a closed issue' + issue.save! + issue.reload + assert_equal was_closed_on, issue.closed_on + end + + def test_closed_on_should_be_preserved_when_reopening_a_closed_issue + issue = Issue.open(false).first + was_closed_on = issue.closed_on + assert_not_nil was_closed_on + issue.subject = 'Reopening a closed issue' + issue.status_id = 1 + issue.save! + issue.reload + assert !issue.closed? + assert_equal was_closed_on, issue.closed_on + end + + def test_status_was_should_return_nil_for_new_issue + issue = Issue.new + assert_nil issue.status_was + end + + def test_status_was_should_return_status_before_change + issue = Issue.find(1) + issue.status = IssueStatus.find(2) + assert_equal IssueStatus.find(1), issue.status_was + end + + def test_status_was_should_be_reset_on_save + issue = Issue.find(1) + issue.status = IssueStatus.find(2) + assert_equal IssueStatus.find(1), issue.status_was + assert issue.save! + assert_equal IssueStatus.find(2), issue.status_was + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/issue_transaction_test.rb --- a/test/unit/issue_transaction_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/issue_transaction_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -30,14 +30,13 @@ self.use_transactional_fixtures = false def test_invalid_move_to_another_project + lft1 = new_issue_lft 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] - + assert_equal [1, parent1.id, lft1, lft1 + 5], [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) @@ -45,10 +44,9 @@ 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] + assert_equal [1, parent1.id, lft1, lft1 + 5], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [1, parent1.id, lft1 + 1, lft1 + 4], [child.project_id, child.root_id, child.lft, child.rgt] + assert_equal [1, parent1.id, lft1 + 2, lft1 + 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] end end diff -r d98d22a98252 -r fb9a13467253 test/unit/journal_observer_test.rb --- a/test/unit/journal_observer_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/journal_observer_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -29,8 +29,8 @@ # context: issue_updated notified_events def test_create_should_send_email_notification_with_issue_updated - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first journal = issue.init_journal(user, issue) with_settings :notified_events => %w(issue_updated) do @@ -40,8 +40,8 @@ end def test_create_should_not_send_email_notification_with_notify_set_to_false - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first journal = issue.init_journal(user, issue) journal.notify = false @@ -52,8 +52,8 @@ end def test_create_should_not_send_email_notification_without_issue_updated - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first journal = issue.init_journal(user, issue) with_settings :notified_events => [] do @@ -64,8 +64,8 @@ # context: issue_note_added notified_events def test_create_should_send_email_notification_with_issue_note_added - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first journal = issue.init_journal(user, issue) journal.notes = 'This update has a note' @@ -76,8 +76,8 @@ end def test_create_should_not_send_email_notification_without_issue_note_added - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first journal = issue.init_journal(user, issue) journal.notes = 'This update has a note' @@ -89,8 +89,8 @@ # context: issue_status_updated notified_events def test_create_should_send_email_notification_with_issue_status_updated - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first issue.init_journal(user, issue) issue.status = IssueStatus.last @@ -101,8 +101,8 @@ end def test_create_should_not_send_email_notification_without_issue_status_updated - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first issue.init_journal(user, issue) issue.status = IssueStatus.last @@ -114,8 +114,8 @@ # context: issue_priority_updated notified_events def test_create_should_send_email_notification_with_issue_priority_updated - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first issue.init_journal(user, issue) issue.priority = IssuePriority.last @@ -126,8 +126,8 @@ end def test_create_should_not_send_email_notification_without_issue_priority_updated - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first issue.init_journal(user, issue) issue.priority = IssuePriority.last diff -r d98d22a98252 -r fb9a13467253 test/unit/journal_test.rb --- a/test/unit/journal_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/journal_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -41,8 +41,8 @@ def test_create_should_send_email_notification ActionMailer::Base.deliveries.clear - issue = Issue.find(:first) - user = User.find(:first) + issue = Issue.first + user = User.first journal = issue.init_journal(user, issue) assert journal.save @@ -137,7 +137,7 @@ user.reload journals = Journal.visible(user).all assert journals.empty? - # User should see issues of projects for which he has view_issues permissions only + # User should see issues of projects for which user has view_issues permissions only Member.create!(:principal => user, :project_id => 1, :role_ids => [1]) user.reload journals = Journal.visible(user).all @@ -151,7 +151,71 @@ assert user.projects.empty? journals = Journal.visible(user).all assert journals.any? - # Admin should see issues on private projects that he does not belong to + # Admin should see issues on private projects that admin does not belong to assert journals.detect {|journal| !journal.issue.project.is_public?} end + + def test_preload_journals_details_custom_fields_should_set_custom_field_instance_variable + d = JournalDetail.new(:property => 'cf', :prop_key => '2') + journals = [Journal.new(:details => [d])] + + d.expects(:instance_variable_set).with("@custom_field", CustomField.find(2)).once + Journal.preload_journals_details_custom_fields(journals) + end + + def test_preload_journals_details_custom_fields_with_empty_set + assert_nothing_raised do + Journal.preload_journals_details_custom_fields([]) + end + end + + def test_details_should_normalize_dates + j = JournalDetail.create!(:old_value => Date.parse('2012-11-03'), :value => Date.parse('2013-01-02')) + j.reload + assert_equal '2012-11-03', j.old_value + assert_equal '2013-01-02', j.value + end + + def test_details_should_normalize_true_values + j = JournalDetail.create!(:old_value => true, :value => true) + j.reload + assert_equal '1', j.old_value + assert_equal '1', j.value + end + + def test_details_should_normalize_false_values + j = JournalDetail.create!(:old_value => false, :value => false) + j.reload + assert_equal '0', j.old_value + assert_equal '0', j.value + end + + def test_custom_field_should_return_custom_field_for_cf_detail + d = JournalDetail.new(:property => 'cf', :prop_key => '2') + assert_equal CustomField.find(2), d.custom_field + end + + def test_custom_field_should_return_nil_for_non_cf_detail + d = JournalDetail.new(:property => 'subject') + assert_equal nil, d.custom_field + end + + def test_visible_details_should_include_relations_to_visible_issues_only + issue = Issue.generate! + visible_issue = Issue.generate! + IssueRelation.create!(:issue_from => issue, :issue_to => visible_issue, :relation_type => 'relates') + hidden_issue = Issue.generate!(:is_private => true) + IssueRelation.create!(:issue_from => issue, :issue_to => hidden_issue, :relation_type => 'relates') + issue.reload + assert_equal 1, issue.journals.size + journal = issue.journals.first + assert_equal 2, journal.details.size + + visible_details = journal.visible_details(User.anonymous) + assert_equal 1, visible_details.size + assert_equal visible_issue.id.to_s, visible_details.first.value + + visible_details = journal.visible_details(User.find(2)) + assert_equal 2, visible_details.size + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/access_control_test.rb --- a/test/unit/lib/redmine/access_control_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/access_control_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/ciphering_test.rb --- a/test/unit/lib/redmine/ciphering_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/ciphering_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -57,7 +57,7 @@ end Redmine::Configuration.with 'database_cipher_key' => 'secret' do - r = Repository.first(:order => 'id DESC') + r = Repository.order('id DESC').first assert_equal 'clear', r.password end end @@ -68,7 +68,7 @@ end Redmine::Configuration.with 'database_cipher_key' => '' do - r = Repository.first(:order => 'id DESC') + r = Repository.order('id DESC').first # password can not be deciphered assert_nothing_raised do assert r.password.match(/\Aaes-256-cbc:.+\Z/) @@ -85,7 +85,7 @@ Redmine::Configuration.with 'database_cipher_key' => 'secret' do assert Repository.encrypt_all(:password) - r = Repository.first(:order => 'id DESC') + r = Repository.order('id DESC').first assert_equal 'bar', r.password assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/) end @@ -98,7 +98,7 @@ Repository::Subversion.create!(:password => 'bar', :url => 'file:///tmp', :identifier => 'bar') assert Repository.decrypt_all(:password) - r = Repository.first(:order => 'id DESC') + r = Repository.order('id DESC').first assert_equal 'bar', r.password assert_equal 'bar', r.read_attribute(:password) end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/codeset_util_test.rb --- a/test/unit/lib/redmine/codeset_util_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/codeset_util_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/configuration_test.rb --- a/test/unit/lib/redmine/configuration_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/configuration_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/export/pdf_test.rb --- a/test/unit/lib/redmine/export/pdf_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/export/pdf_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../../../../test_helper', __FILE__) -require 'iconv' class PdfTest < ActiveSupport::TestCase fixtures :users, :projects, :roles, :members, :member_roles, @@ -36,9 +35,9 @@ txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_1, encoding) txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_2, encoding) txt_3 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_3, encoding) - assert_equal "?\x91\xd4", txt_1 - assert_equal "?\x91\xd4?", txt_2 - assert_equal "??\x91\xd4?", txt_3 + assert_equal "?\x91\xd4".force_encoding("ASCII-8BIT"), txt_1 + assert_equal "?\x91\xd4?".force_encoding("ASCII-8BIT"), txt_2 + assert_equal "??\x91\xd4?".force_encoding("ASCII-8BIT"), txt_3 assert_equal "ASCII-8BIT", txt_1.encoding.to_s assert_equal "ASCII-8BIT", txt_2.encoding.to_s assert_equal "ASCII-8BIT", txt_3.encoding.to_s diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/bool_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/bool_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,63 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::BoolFieldFormatTest < ActionView::TestCase + include ApplicationHelper + include Redmine::I18n + + def setup + set_language_if_valid 'en' + end + + def test_check_box_style_should_render_edit_tag_as_check_box + field = IssueCustomField.new(:field_format => 'bool', :is_required => false, :edit_tag_style => 'check_box') + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'abc', 'xyz', value) + assert_select_in tag, 'input[name=xyz]', 2 + assert_select_in tag, 'input[id=abc]', 1 + assert_select_in tag, 'input[type=hidden][value=0]' + assert_select_in tag, 'input[type=checkbox][value=1]' + end + + def test_check_box_should_be_checked_when_value_is_set + field = IssueCustomField.new(:field_format => 'bool', :is_required => false, :edit_tag_style => 'check_box') + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => '1') + + tag = field.format.edit_tag(self, 'abc', 'xyz', value) + assert_select_in tag, 'input[type=checkbox][value=1][checked=checked]' + end + + def test_radio_style_should_render_edit_tag_as_radio_buttons + field = IssueCustomField.new(:field_format => 'bool', :is_required => false, :edit_tag_style => 'radio') + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'abc', 'xyz', value) + assert_select_in tag, 'input[type=radio][name=xyz]', 3 + end + + def test_default_style_should_render_edit_tag_as_select + field = IssueCustomField.new(:field_format => 'bool', :is_required => false) + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'abc', 'xyz', value) + assert_select_in tag, 'select[name=xyz]', 1 + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/field_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/field_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::FieldFormatTest < ActionView::TestCase + include ApplicationHelper + + def test_string_field_with_text_formatting_disabled_should_not_format_text + field = IssueCustomField.new(:field_format => 'string') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*") + + assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, false) + assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, true) + end + + def test_string_field_with_text_formatting_enabled_should_format_text + field = IssueCustomField.new(:field_format => 'string', :text_formatting => 'full') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*") + + assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, false) + assert_include "foo", field.format.formatted_custom_value(self, custom_value, true) + end + + def test_text_field_with_text_formatting_disabled_should_not_format_text + field = IssueCustomField.new(:field_format => 'text') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*\nbar") + + assert_equal "*foo*\nbar", field.format.formatted_custom_value(self, custom_value, false) + assert_include "*foo*\n
    bar", field.format.formatted_custom_value(self, custom_value, true) + end + + def test_text_field_with_text_formatting_enabled_should_format_text + field = IssueCustomField.new(:field_format => 'text', :text_formatting => 'full') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*\nbar") + + assert_equal "*foo*\nbar", field.format.formatted_custom_value(self, custom_value, false) + assert_include "foo", field.format.formatted_custom_value(self, custom_value, true) + end + + def test_text_field_with_url_pattern_should_format_as_link + field = IssueCustomField.new(:field_format => 'string', :url_pattern => 'http://foo/%value%') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "bar") + + assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'bar', field.format.formatted_custom_value(self, custom_value, true) + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/link_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/link_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,90 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::LinkFieldFormatTest < ActionView::TestCase + def test_link_field_should_substitute_value + field = IssueCustomField.new(:field_format => 'link', :url_pattern => 'http://foo/%value%') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "bar") + + assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'bar', field.format.formatted_custom_value(self, custom_value, true) + end + + def test_link_field_should_substitute_object_id_in_url + object = Issue.new + object.stubs(:id).returns(10) + + field = IssueCustomField.new(:field_format => 'link', :url_pattern => 'http://foo/%id%') + custom_value = CustomValue.new(:custom_field => field, :customized => object, :value => "bar") + + assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'bar', field.format.formatted_custom_value(self, custom_value, true) + end + + def test_link_field_should_substitute_project_id_in_url + project = Project.new + project.stubs(:id).returns(52) + object = Issue.new + object.stubs(:project).returns(project) + + field = IssueCustomField.new(:field_format => 'link', :url_pattern => 'http://foo/%project_id%') + custom_value = CustomValue.new(:custom_field => field, :customized => object, :value => "bar") + + assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'bar', field.format.formatted_custom_value(self, custom_value, true) + end + + def test_link_field_should_substitute_project_identifier_in_url + project = Project.new + project.stubs(:identifier).returns('foo_project-00') + object = Issue.new + object.stubs(:project).returns(project) + + field = IssueCustomField.new(:field_format => 'link', :url_pattern => 'http://foo/%project_identifier%') + custom_value = CustomValue.new(:custom_field => field, :customized => object, :value => "bar") + + assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'bar', field.format.formatted_custom_value(self, custom_value, true) + end + + def test_link_field_should_substitute_regexp_groups + field = IssueCustomField.new(:field_format => 'link', :regexp => /^(.+)-(.+)$/, :url_pattern => 'http://foo/%m2%/%m1%') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "56-142") + + assert_equal "56-142", field.format.formatted_custom_value(self, custom_value, false) + assert_equal '56-142', field.format.formatted_custom_value(self, custom_value, true) + end + + def test_link_field_without_url_pattern_should_link_to_value + field = IssueCustomField.new(:field_format => 'link') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "http://foo/bar") + + assert_equal "http://foo/bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'http://foo/bar', field.format.formatted_custom_value(self, custom_value, true) + end + + def test_link_field_without_url_pattern_should_link_to_value_with_http_by_default + field = IssueCustomField.new(:field_format => 'link') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "foo.bar") + + assert_equal "foo.bar", field.format.formatted_custom_value(self, custom_value, false) + assert_equal 'foo.bar', field.format.formatted_custom_value(self, custom_value, true) + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/list_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/list_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,168 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::ListFieldFormatTest < ActionView::TestCase + include ApplicationHelper + include Redmine::I18n + + def setup + set_language_if_valid 'en' + end + + def test_possible_existing_value_should_be_valid + field = GroupCustomField.create!(:name => 'List', :field_format => 'list', :possible_values => ['Foo', 'Bar']) + group = Group.new(:name => 'Group') + group.custom_field_values = {field.id => 'Baz'} + assert group.save(:validate => false) + + group = Group.order('id DESC').first + assert_equal ['Foo', 'Bar', 'Baz'], field.possible_custom_value_options(group.custom_value_for(field)) + assert group.valid? + end + + def test_edit_tag_should_have_id_and_name + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false) + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'abc', 'xyz', value) + assert_select_in tag, 'select[id=abc][name=xyz]' + end + + def test_edit_tag_should_contain_possible_values + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false) + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'select' do + assert_select 'option', 3 + assert_select 'option[value=]' + assert_select 'option[value=Foo]', :text => 'Foo' + assert_select 'option[value=Bar]', :text => 'Bar' + end + end + + def test_edit_tag_should_select_current_value + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false) + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => 'Bar') + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'select' do + assert_select 'option[selected=selected]', 1 + assert_select 'option[value=Bar][selected=selected]', :text => 'Bar' + end + end + + def test_edit_tag_with_multiple_should_select_current_values + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar', 'Baz'], :is_required => false, + :multiple => true) + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => ['Bar', 'Baz']) + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'select[multiple=multiple]' do + assert_select 'option[selected=selected]', 2 + assert_select 'option[value=Bar][selected=selected]', :text => 'Bar' + assert_select 'option[value=Baz][selected=selected]', :text => 'Baz' + end + end + + def test_edit_tag_with_check_box_style_should_contain_possible_values + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false, + :edit_tag_style => 'check_box') + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'span' do + assert_select 'input[type=radio]', 3 + assert_select 'label', :text => '(none)' do + assert_select 'input[value=]' + end + assert_select 'label', :text => 'Foo' do + assert_select 'input[value=Foo]' + end + assert_select 'label', :text => 'Bar' do + assert_select 'input[value=Bar]' + end + end + end + + def test_edit_tag_with_check_box_style_should_select_current_value + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false, + :edit_tag_style => 'check_box') + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => 'Bar') + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'span' do + assert_select 'input[type=radio][checked=checked]', 1 + assert_select 'label', :text => 'Bar' do + assert_select 'input[value=Bar][checked=checked]' + end + end + end + + def test_edit_tag_with_check_box_style_and_multiple_values_should_contain_hidden_field_to_clear_value + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false, + :edit_tag_style => 'check_box', :multiple => true) + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new) + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'span' do + assert_select 'input[type=checkbox]', 2 + assert_select 'input[type=hidden]', 1 + end + end + + def test_field_with_url_pattern_should_link_value + field = IssueCustomField.new(:field_format => 'list', :url_pattern => 'http://localhost/%value%') + formatted = field.format.formatted_value(self, field, 'foo', Issue.new, true) + assert_equal 'foo', formatted + assert formatted.html_safe? + end + + def test_field_with_url_pattern_and_multiple_values_should_link_values + field = IssueCustomField.new(:field_format => 'list', :url_pattern => 'http://localhost/%value%') + formatted = field.format.formatted_value(self, field, ['foo', 'bar'], Issue.new, true) + assert_equal 'bar, foo', formatted + assert formatted.html_safe? + end + + def test_field_with_url_pattern_should_not_link_blank_value + field = IssueCustomField.new(:field_format => 'list', :url_pattern => 'http://localhost/%value%') + formatted = field.format.formatted_value(self, field, '', Issue.new, true) + assert_equal '', formatted + assert formatted.html_safe? + end + + def test_edit_tag_with_check_box_style_and_multiple_should_select_current_values + field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar', 'Baz'], :is_required => false, + :multiple => true, :edit_tag_style => 'check_box') + value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => ['Bar', 'Baz']) + + tag = field.format.edit_tag(self, 'id', 'name', value) + assert_select_in tag, 'span' do + assert_select 'input[type=checkbox][checked=checked]', 2 + assert_select 'label', :text => 'Bar' do + assert_select 'input[value=Bar][checked=checked]' + end + assert_select 'label', :text => 'Baz' do + assert_select 'input[value=Baz][checked=checked]' + end + end + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/numeric_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/numeric_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,31 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::NumericFieldFormatTest < ActionView::TestCase + include ApplicationHelper + + def test_integer_field_with_url_pattern_should_format_as_link + field = IssueCustomField.new(:field_format => 'int', :url_pattern => 'http://foo/%value%') + custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "3") + + assert_equal 3, field.format.formatted_custom_value(self, custom_value, false) + assert_equal '3', field.format.formatted_custom_value(self, custom_value, true) + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/user_field_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/user_field_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,58 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::UserFieldFormatTest < ActionView::TestCase + fixtures :projects, :roles, :users, :members, :member_roles + + def test_user_role_should_reject_blank_values + field = IssueCustomField.new(:name => 'Foo', :field_format => 'user', :user_role => ["1", ""]) + field.save! + assert_equal ["1"], field.user_role + end + + def test_existing_values_should_be_valid + field = IssueCustomField.create!(:name => 'Foo', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all) + project = Project.generate! + user = User.generate! + User.add_to_project(user, project, Role.find_by_name('Manager')) + issue = Issue.generate!(:project_id => project.id, :tracker_id => 1, :custom_field_values => {field.id => user.id}) + + field.user_role = [Role.find_by_name('Developer').id] + field.save! + + issue = Issue.order('id DESC').first + assert_include [user.name, user.id.to_s], field.possible_custom_value_options(issue.custom_value_for(field)) + assert issue.valid? + end + + def test_possible_values_options_should_return_project_members + field = IssueCustomField.new(:field_format => 'user') + project = Project.find(1) + + assert_equal ['Dave Lopper', 'John Smith'], field.possible_values_options(project).map(&:first) + end + + def test_possible_values_options_should_return_project_members_with_selected_role + field = IssueCustomField.new(:field_format => 'user', :user_role => ["2"]) + project = Project.find(1) + + assert_equal ['Dave Lopper'], field.possible_values_options(project).map(&:first) + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/field_format/version_field_format_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/field_format/version_field_format_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,66 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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__) +require 'redmine/field_format' + +class Redmine::VersionFieldFormatTest < ActionView::TestCase + fixtures :projects, :versions, :trackers + + def test_version_status_should_reject_blank_values + field = IssueCustomField.new(:name => 'Foo', :field_format => 'version', :version_status => ["open", ""]) + field.save! + assert_equal ["open"], field.version_status + end + + def test_existing_values_should_be_valid + field = IssueCustomField.create!(:name => 'Foo', :field_format => 'version', :is_for_all => true, :trackers => Tracker.all) + project = Project.generate! + version = Version.generate!(:project => project, :status => 'open') + issue = Issue.generate!(:project_id => project.id, :tracker_id => 1, :custom_field_values => {field.id => version.id}) + + field.version_status = ["open"] + field.save! + + issue = Issue.order('id DESC').first + assert_include [version.name, version.id.to_s], field.possible_custom_value_options(issue.custom_value_for(field)) + assert issue.valid? + end + + def test_possible_values_options_should_return_project_versions + field = IssueCustomField.new(:field_format => 'version') + project = Project.find(1) + expected = project.shared_versions.sort.map(&:name) + + assert_equal expected, field.possible_values_options(project).map(&:first) + end + + def test_possible_values_options_should_return_project_versions_with_selected_status + field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"]) + project = Project.find(1) + expected = project.shared_versions.sort.select {|v| v.status == "open"}.map(&:name) + + assert_equal expected, field.possible_values_options(project).map(&:first) + end + + def test_cast_value_should_not_raise_error_when_array_contains_value_casted_to_nil + field = IssueCustomField.new(:field_format => 'version') + assert_nothing_raised do + field.cast_value([1,2, 42]) + end + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/helpers/calendar_test.rb --- a/test/unit/lib/redmine/helpers/calendar_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/helpers/calendar_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/helpers/diff_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/helpers/diff_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,25 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 DiffTest < ActiveSupport::TestCase + def test_diff + diff = Redmine::Helpers::Diff.new("foo", "bar") + assert_not_nil diff + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/helpers/gantt_test.rb --- a/test/unit/lib/redmine/helpers/gantt_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/helpers/gantt_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -26,14 +26,13 @@ :member_roles, :members, :enabled_modules, - :workflows, :versions, :groups_users - include ApplicationHelper include ProjectsHelper include IssuesHelper include ERB::Util + include Rails.application.routes.url_helpers def setup setup_with_controller @@ -43,191 +42,186 @@ def today @today ||= Date.today end + private :today # Creates a Gantt chart for a 4 week span def create_gantt(project=Project.generate!, options={}) @project = project @gantt = Redmine::Helpers::Gantt.new(options) @gantt.project = @project - @gantt.query = Query.create!(:project => @project, :name => 'Gantt') + @gantt.query = IssueQuery.create!(:project => @project, :name => 'Gantt') @gantt.view = self @gantt.instance_variable_set('@date_from', options[:date_from] || (today - 14)) @gantt.instance_variable_set('@date_to', options[:date_to] || (today + 14)) end + private :create_gantt - context "#number_of_rows" do - context "with one project" do - should "return the number of rows just for that project" - end - - context "with no project" do - should "return the total number of rows for all the projects, resursively" - end - - should "not exceed max_rows option" do - p = Project.generate! - 5.times do - Issue.generate!(:project => p) - end - create_gantt(p) - @gantt.render - assert_equal 6, @gantt.number_of_rows - assert !@gantt.truncated - create_gantt(p, :max_rows => 3) - @gantt.render - assert_equal 3, @gantt.number_of_rows - assert @gantt.truncated - end + test "#number_of_rows with one project should return the number of rows just for that project" do + p1, p2 = Project.generate!, Project.generate! + i1, i2 = Issue.generate!(:project => p1), Issue.generate!(:project => p2) + create_gantt(p1) + assert_equal 2, @gantt.number_of_rows end - context "#number_of_rows_on_project" do - setup do - create_gantt - end - - should "count 0 for an empty the project" do - assert_equal 0, @gantt.number_of_rows_on_project(@project) - end - - should "count the number of issues without a version" do - @project.issues << Issue.generate!(:project => @project, :fixed_version => nil) - assert_equal 2, @gantt.number_of_rows_on_project(@project) - end - - should "count the number of issues on versions, including cross-project" do - version = Version.generate! - @project.versions << version - @project.issues << Issue.generate!(:project => @project, :fixed_version => version) - assert_equal 3, @gantt.number_of_rows_on_project(@project) - end + test "#number_of_rows with no project should return the total number of rows for all the projects, resursively" do + p1, p2 = Project.generate!, Project.generate! + create_gantt(nil) + # fix the return value of #number_of_rows_on_project() to an arbitrary value + # so that we really only test #number_of_rows + @gantt.stubs(:number_of_rows_on_project).returns(7) + # also fix #projects because we want to test #number_of_rows in isolation + @gantt.stubs(:projects).returns(Project.all) + # actual test + assert_equal Project.count*7, @gantt.number_of_rows end - # TODO: more of an integration test - context "#subjects" do - setup do - create_gantt - @project.enabled_module_names = [:issue_tracking] - @tracker = Tracker.generate! - @project.trackers << @tracker - @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none') - @project.versions << @version - @issue = Issue.generate!(:fixed_version => @version, + test "#number_of_rows should not exceed max_rows option" do + p = Project.generate! + 5.times do + Issue.generate!(:project => p) + end + create_gantt(p) + @gantt.render + assert_equal 6, @gantt.number_of_rows + assert !@gantt.truncated + create_gantt(p, :max_rows => 3) + @gantt.render + assert_equal 3, @gantt.number_of_rows + assert @gantt.truncated + end + + test "#number_of_rows_on_project should count 0 for an empty the project" do + create_gantt + assert_equal 0, @gantt.number_of_rows_on_project(@project) + end + + test "#number_of_rows_on_project should count the number of issues without a version" do + create_gantt + @project.issues << Issue.generate!(:project => @project, :fixed_version => nil) + assert_equal 2, @gantt.number_of_rows_on_project(@project) + end + + test "#number_of_rows_on_project should count the number of issues on versions, including cross-project" do + create_gantt + version = Version.generate! + @project.versions << version + @project.issues << Issue.generate!(:project => @project, :fixed_version => version) + assert_equal 3, @gantt.number_of_rows_on_project(@project) + end + + def setup_subjects + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none') + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, :subject => "gantt#line_for_project", :tracker => @tracker, :project => @project, :done_ratio => 30, :start_date => (today - 1), :due_date => (today + 7)) - @project.issues << @issue - end + @project.issues << @issue + end + private :setup_subjects - context "project" do - should "be rendered" do - @output_buffer = @gantt.subjects - assert_select "div.project-name a", /#{@project.name}/ - end + # TODO: more of an integration test + test "#subjects project should be rendered" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.project-name a", /#{@project.name}/ + end - should "have an indent of 4" do - @output_buffer = @gantt.subjects - assert_select "div.project-name[style*=left:4px]" - end - end + test "#subjects project should have an indent of 4" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.project-name[style*=left:4px]" + end - context "version" do - should "be rendered" do - @output_buffer = @gantt.subjects - assert_select "div.version-name a", /#{@version.name}/ - end + test "#subjects version should be rendered" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.version-name a", /#{@version.name}/ + end - should "be indented 24 (one level)" do - @output_buffer = @gantt.subjects - assert_select "div.version-name[style*=left:24px]" - end + test "#subjects version should be indented 24 (one level)" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.version-name[style*=left:24px]" + end - context "without assigned issues" do - setup do - @version = Version.generate!(:effective_date => (today + 14), + test "#subjects version without assigned issues should not be rendered" do + setup_subjects + @version = Version.generate!(:effective_date => (today + 14), :sharing => 'none', :name => 'empty_version') - @project.versions << @version - end + @project.versions << @version + @output_buffer = @gantt.subjects + assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0 + end - should "not be rendered" do - @output_buffer = @gantt.subjects - assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0 - end - end - end + test "#subjects issue should be rendered" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.issue-subject", /#{@issue.subject}/ + end - context "issue" do - should "be rendered" do - @output_buffer = @gantt.subjects - assert_select "div.issue-subject", /#{@issue.subject}/ - end + test "#subjects issue should be indented 44 (two levels)" do + setup_subjects + @output_buffer = @gantt.subjects + assert_select "div.issue-subject[style*=left:44px]" + end - should "be indented 44 (two levels)" do - @output_buffer = @gantt.subjects - assert_select "div.issue-subject[style*=left:44px]" - end - - context "assigned to a shared version of another project" do - setup do - p = Project.generate! - p.enabled_module_names = [:issue_tracking] - @shared_version = Version.generate!(:sharing => 'system') - p.versions << @shared_version - # Reassign the issue to a shared version of another project - @issue = Issue.generate!(:fixed_version => @shared_version, + test "#subjects issue assigned to a shared version of another project should be rendered" do + setup_subjects + p = Project.generate! + p.enabled_module_names = [:issue_tracking] + @shared_version = Version.generate!(:sharing => 'system') + p.versions << @shared_version + # Reassign the issue to a shared version of another project + @issue = Issue.generate!(:fixed_version => @shared_version, :subject => "gantt#assigned_to_shared_version", :tracker => @tracker, :project => @project, :done_ratio => 30, :start_date => (today - 1), :due_date => (today + 7)) - @project.issues << @issue - end + @project.issues << @issue + @output_buffer = @gantt.subjects + assert_select "div.issue-subject", /#{@issue.subject}/ + end - should "be rendered" do - @output_buffer = @gantt.subjects - assert_select "div.issue-subject", /#{@issue.subject}/ - end - end - - context "with subtasks" do - setup do - attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version} - @child1 = Issue.generate!( + test "#subjects issue with subtasks should indent subtasks" do + setup_subjects + attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version} + @child1 = Issue.generate!( attrs.merge(:subject => 'child1', :parent_issue_id => @issue.id, :start_date => (today - 1), :due_date => (today + 2)) ) - @child2 = Issue.generate!( + @child2 = Issue.generate!( attrs.merge(:subject => 'child2', :parent_issue_id => @issue.id, :start_date => today, :due_date => (today + 7)) ) - @grandchild = Issue.generate!( + @grandchild = Issue.generate!( attrs.merge(:subject => 'grandchild', :parent_issue_id => @child1.id, :start_date => (today - 1), :due_date => (today + 2)) ) - end - - should "indent subtasks" do - @output_buffer = @gantt.subjects - # parent task 44px - assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/ - # children 64px - assert_select "div.issue-subject[style*=left:64px]", /child1/ - assert_select "div.issue-subject[style*=left:64px]", /child2/ - # grandchild 84px - assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer - end - end - end + @output_buffer = @gantt.subjects + # parent task 44px + assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/ + # children 64px + assert_select "div.issue-subject[style*=left:64px]", /child1/ + assert_select "div.issue-subject[style*=left:64px]", /child2/ + # grandchild 84px + assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer end context "#lines" do @@ -276,18 +270,6 @@ end end - context "#render_project" do - should "be tested" - end - - context "#render_issues" do - should "be tested" - end - - context "#render_version" do - should "be tested" - end - context "#subject_for_project" do setup do create_gantt @@ -322,8 +304,6 @@ assert_select 'div span.project-overdue' end end - should "test the PNG format" - should "test the PDF format" end context "#line_for_project" do @@ -356,30 +336,6 @@ end end - context "late line" do - should_eventually "start from the starting point on the left" do - @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) - assert_select "div.project.task_late[style*=left:28px]", true, @output_buffer - end - - should_eventually "be the total delayed width of the project" do - @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) - assert_select "div.project.task_late[style*=width:30px]", true, @output_buffer - end - end - - context "done line" do - should_eventually "start from the starting point on the left" do - @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) - assert_select "div.project.task_done[style*=left:28px]", true, @output_buffer - end - - should_eventually "Be the total done width of the project" do - @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) - assert_select "div.project.task_done[style*=width:18px]", true, @output_buffer - end - end - context "starting marker" do should "not appear if the starting point is off the gantt chart" do # Shift the date range of the chart @@ -419,15 +375,8 @@ @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) assert_select "div.project.label", /#{@project.name}/ end - - should_eventually "show the percent complete" do - @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) - assert_select "div.project.label", /0%/ - end end end - should "test the PNG format" - should "test the PDF format" end context "#subject_for_version" do @@ -479,8 +428,6 @@ assert_select 'div span.version-behind-schedule' end end - should "test the PNG format" - should "test the PDF format" end context "#line_for_version" do @@ -583,8 +530,6 @@ end end end - should "test the PNG format" - should "test the PDF format" end context "#subject_for_issue" do @@ -628,8 +573,6 @@ assert_select 'div span.issue-overdue' end end - should "test the PNG format" - should "test the PDF format" end context "#line_for_issue" do @@ -735,15 +678,82 @@ @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) assert_select "div.tooltip", /#{@issue.subject}/ end - should "test the PNG format" - should "test the PDF format" end - context "#to_image" do - should "be tested" + def test_sort_issues_no_date + project = Project.generate! + issue1 = Issue.generate!(:subject => "test", :project => project) + issue2 = Issue.generate!(:subject => "test", :project => project) + assert issue1.root_id < issue2.root_id + child1 = Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project) + child2 = Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project) + child3 = Issue.generate!(:parent_issue_id => child1.id, :subject => 'child', + :project => project) + assert_equal child1.root_id, child2.root_id + assert child1.lft < child2.lft + assert child3.lft < child2.lft + issues = [child3, child2, child1, issue2, issue1] + Redmine::Helpers::Gantt.sort_issues!(issues) + assert_equal [issue1.id, child1.id, child3.id, child2.id, issue2.id], + issues.map{|v| v.id} end - context "#to_pdf" do - should "be tested" + def test_sort_issues_root_only + project = Project.generate! + issue1 = Issue.generate!(:subject => "test", :project => project) + issue2 = Issue.generate!(:subject => "test", :project => project) + issue3 = Issue.generate!(:subject => "test", :project => project, + :start_date => (today - 1)) + issue4 = Issue.generate!(:subject => "test", :project => project, + :start_date => (today - 2)) + issues = [issue4, issue3, issue2, issue1] + Redmine::Helpers::Gantt.sort_issues!(issues) + assert_equal [issue1.id, issue2.id, issue4.id, issue3.id], + issues.map{|v| v.id} + end + + def test_sort_issues_tree + project = Project.generate! + issue1 = Issue.generate!(:subject => "test", :project => project) + issue2 = Issue.generate!(:subject => "test", :project => project, + :start_date => (today - 2)) + issue1_child1 = + Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project) + issue1_child2 = + Issue.generate!(:parent_issue_id => issue1.id, :subject => 'child', + :project => project, :start_date => (today - 10)) + issue1_child1_child1 = + Issue.generate!(:parent_issue_id => issue1_child1.id, :subject => 'child', + :project => project, :start_date => (today - 8)) + issue1_child1_child2 = + Issue.generate!(:parent_issue_id => issue1_child1.id, :subject => 'child', + :project => project, :start_date => (today - 9)) + issue1_child1_child1_logic = Redmine::Helpers::Gantt.sort_issue_logic(issue1_child1_child1) + assert_equal [[today - 10, issue1.id], [today - 9, issue1_child1.id], + [today - 8, issue1_child1_child1.id]], + issue1_child1_child1_logic + issue1_child1_child2_logic = Redmine::Helpers::Gantt.sort_issue_logic(issue1_child1_child2) + assert_equal [[today - 10, issue1.id], [today - 9, issue1_child1.id], + [today - 9, issue1_child1_child2.id]], + issue1_child1_child2_logic + issues = [issue1_child1_child2, issue1_child1_child1, issue1_child2, + issue1_child1, issue2, issue1] + Redmine::Helpers::Gantt.sort_issues!(issues) + assert_equal [issue1.id, issue1_child1.id, issue1_child2.id, + issue1_child1_child2.id, issue1_child1_child1.id, issue2.id], + issues.map{|v| v.id} + end + + def test_sort_versions + project = Project.generate! + version1 = Version.create!(:project => project, :name => 'test1') + version2 = Version.create!(:project => project, :name => 'test2', :effective_date => '2013-10-25') + version3 = Version.create!(:project => project, :name => 'test3') + version4 = Version.create!(:project => project, :name => 'test4', :effective_date => '2013-10-02') + + assert_equal versions.sort, Redmine::Helpers::Gantt.sort_versions!(versions) end end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/hook_test.rb --- a/test/unit/lib/redmine/hook_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/hook_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -23,7 +23,7 @@ :trackers, :projects_trackers, :enabled_modules, :versions, - :issue_statuses, :issue_categories, :issue_relations, :workflows, + :issue_statuses, :issue_categories, :issue_relations, :enumerations, :issues @@ -154,14 +154,14 @@ issue = Issue.find(1) ActionMailer::Base.deliveries.clear - Mailer.issue_add(issue).deliver + Mailer.deliver_issue_add(issue) 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 + Mailer.deliver_issue_add(issue) mail2 = ActionMailer::Base.deliveries.last assert_equal mail_body(mail), mail_body(mail2) diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/i18n_test.rb --- a/test/unit/lib/redmine/i18n_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/i18n_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -105,12 +105,12 @@ now = Time.parse('2011-02-20 15:45:22') with_settings :time_format => '' do with_settings :date_format => '' do - assert_equal '02/20/2011 03:45 pm', format_time(now) - assert_equal '03:45 pm', format_time(now, false) + assert_equal '02/20/2011 03:45 PM', format_time(now) + assert_equal '03:45 PM', format_time(now, false) end with_settings :date_format => '%Y-%m-%d' do - assert_equal '2011-02-20 03:45 pm', format_time(now) - assert_equal '03:45 pm', format_time(now, false) + assert_equal '2011-02-20 03:45 PM', format_time(now) + assert_equal '03:45 PM', format_time(now, false) end end end @@ -131,15 +131,6 @@ end end - def test_time_format - set_language_if_valid 'en' - now = Time.now - Setting.date_format = '%d %m %Y' - Setting.time_format = '%H %M' - assert_equal now.strftime('%d %m %Y %H %M'), format_time(now) - assert_equal now.strftime('%H %M'), format_time(now, false) - end - def test_utc_time_format set_language_if_valid 'en' now = Time.now @@ -196,13 +187,15 @@ def test_languages_options options = languages_options - assert options.is_a?(Array) assert_equal valid_languages.size, options.size assert_nil options.detect {|option| !option.is_a?(Array)} assert_nil options.detect {|option| option.size != 2} assert_nil options.detect {|option| !option.first.is_a?(String) || !option.last.is_a?(String)} assert_include ["English", "en"], options + ja = "Japanese (\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e)" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + assert_include [ja, "ja"], options end def test_locales_validness diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/info_test.rb --- a/test/unit/lib/redmine/info_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/info_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/menu_manager/mapper_test.rb --- a/test/unit/lib/redmine/menu_manager/mapper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/menu_manager/mapper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,8 +18,17 @@ require File.expand_path('../../../../../test_helper', __FILE__) class Redmine::MenuManager::MapperTest < ActiveSupport::TestCase - context "Mapper#initialize" do - should "be tested" + test "Mapper#initialize should define a root MenuNode if menu is not present in items" do + menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {}) + node = menu_mapper.menu_items + assert_not_nil node + assert_equal :root, node.name + end + + test "Mapper#initialize should use existing MenuNode if present" do + node = "foo" # just an arbitrary reference + menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {:test_menu => node}) + assert_equal node, menu_mapper.menu_items end def test_push_onto_root diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/menu_manager/menu_helper_test.rb --- a/test/unit/lib/redmine/menu_manager/menu_helper_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/menu_manager/menu_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/menu_manager_test.rb --- a/test/unit/lib/redmine/menu_manager_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/menu_manager_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,11 +18,17 @@ require File.expand_path('../../../../test_helper', __FILE__) class Redmine::MenuManagerTest < ActiveSupport::TestCase - context "MenuManager#map" do - should "be tested" + def test_map_should_yield_a_mapper + assert_difference 'Redmine::MenuManager.items(:project_menu).size' do + Redmine::MenuManager.map :project_menu do |mapper| + assert_kind_of Redmine::MenuManager::Mapper, mapper + mapper.push :new_item, '/' + end + end end - context "MenuManager#items" do - should "be tested" + def test_items_should_return_menu_items + items = Redmine::MenuManager.items(:project_menu) + assert_kind_of Redmine::MenuManager::MenuNode, items.first end end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/mime_type_test.rb --- a/test/unit/lib/redmine/mime_type_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/mime_type_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -58,4 +58,9 @@ assert_equal expected, Redmine::MimeType.is_type?(*args) end end + + def test_should_default_to_mime_type_gem + assert !Redmine::MimeType::EXTENSIONS.keys.include?("zip") + assert_equal "application/zip", Redmine::MimeType.of("file.zip") + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/notifiable_test.rb --- a/test/unit/lib/redmine/notifiable_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/notifiable_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/pagination_helper_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/pagination_helper_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,34 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 PaginationHelperTest < ActionView::TestCase + include Redmine::Pagination::Helper + + def test_per_page_options_should_return_usefull_values + with_settings :per_page_options => '10, 25, 50, 100' do + assert_equal [], per_page_options(10, 3) + assert_equal [], per_page_options(25, 3) + assert_equal [10, 25], per_page_options(10, 22) + assert_equal [10, 25], per_page_options(25, 22) + assert_equal [10, 25, 50], per_page_options(50, 22) + assert_equal [10, 25, 50], per_page_options(25, 26) + assert_equal [10, 25, 50, 100], per_page_options(25, 120) + end + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/pagination_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/pagination_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,94 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::PaginationTest < ActiveSupport::TestCase + + def setup + @klass = Redmine::Pagination::Paginator + end + + def test_count_is_zero + p = @klass.new 0, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + %w(first_page previous_page next_page last_page).each do |method| + assert_nil p.send(method), "#{method} was not nil" + end + assert_equal 0, p.first_item + assert_equal 0, p.last_item + assert_equal [], p.linked_pages + end + + def test_count_is_less_than_per_page + p = @klass.new 7, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_nil p.next_page + assert_equal 1, p.last_page + assert_equal 1, p.first_item + assert_equal 7, p.last_item + assert_equal [], p.linked_pages + end + + def test_count_is_equal_to_per_page + p = @klass.new 10, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_nil p.next_page + assert_equal 1, p.last_page + assert_equal 1, p.first_item + assert_equal 10, p.last_item + assert_equal [], p.linked_pages + end + + def test_2_pages + p = @klass.new 16, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_equal 2, p.next_page + assert_equal 2, p.last_page + assert_equal 1, p.first_item + assert_equal 10, p.last_item + assert_equal [1, 2], p.linked_pages + end + + def test_many_pages + p = @klass.new 155, 10, 1 + + assert_equal 0, p.offset + assert_equal 10, p.per_page + assert_equal 1, p.first_page + assert_nil p.previous_page + assert_equal 2, p.next_page + assert_equal 16, p.last_page + assert_equal 1, p.first_item + assert_equal 10, p.last_item + assert_equal [1, 2, 3, 16], p.linked_pages + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/plugin_test.rb --- a/test/unit/lib/redmine/plugin_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/plugin_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,7 +18,6 @@ require File.expand_path('../../../../test_helper', __FILE__) class Redmine::PluginTest < ActiveSupport::TestCase - def setup @klass = Redmine::Plugin # In case some real plugins are installed @@ -55,7 +54,6 @@ def test_installed @klass.register(:foo) {} - assert_equal true, @klass.installed?(:foo) assert_equal false, @klass.installed?(:bar) end @@ -74,7 +72,6 @@ def test_delete_menu_item Redmine::MenuManager.map(:project_menu).push(:foo_menu_item, '/foo', :caption => 'Foo') - assert_difference 'Redmine::MenuManager.items(:project_menu).size', -1 do @klass.register :foo do delete_menu_item :project_menu, :foo_menu_item @@ -83,10 +80,21 @@ assert_nil Redmine::MenuManager.items(:project_menu).detect {|i| i.name == :foo_menu_item} end + def test_directory_with_override + @klass.register(:foo) do + directory '/path/to/foo' + end + assert_equal '/path/to/foo', @klass.find('foo').directory + end + + def test_directory_without_override + @klass.register(:foo) {} + assert_equal File.join(@klass.directory, 'foo'), @klass.find('foo').directory + end + def test_requires_redmine plugin = Redmine::Plugin.register(:foo) {} Redmine::VERSION.stubs(:to_a).returns([2, 1, 3, "stable", 10817]) - # Specific version without hash assert plugin.requires_redmine('2.1.3') assert plugin.requires_redmine('2.1') @@ -96,7 +104,6 @@ assert_raise Redmine::PluginRequirementError do plugin.requires_redmine('2.2') end - # Specific version assert plugin.requires_redmine(:version => '2.1.3') assert plugin.requires_redmine(:version => ['2.1.3', '2.2.0']) @@ -110,7 +117,6 @@ assert_raise Redmine::PluginRequirementError do plugin.requires_redmine(:version => '2.2') end - # Version range assert plugin.requires_redmine(:version => '2.0.0'..'2.2.4') assert plugin.requires_redmine(:version => '2.1.3'..'2.2.4') @@ -121,8 +127,6 @@ assert_raise Redmine::PluginRequirementError do plugin.requires_redmine(:version => '2.1.4'..'2.2.4') end - - # Version or higher assert plugin.requires_redmine(:version_or_higher => '0.1.0') assert plugin.requires_redmine(:version_or_higher => '2.1.3') @@ -138,12 +142,10 @@ def test_requires_redmine_plugin test = self other_version = '0.5.0' - @klass.register :other do name 'Other' version other_version end - @klass.register :foo do test.assert requires_redmine_plugin(:other, :version_or_higher => '0.1.0') test.assert requires_redmine_plugin(:other, :version_or_higher => other_version) @@ -151,7 +153,6 @@ test.assert_raise Redmine::PluginRequirementError do requires_redmine_plugin(:other, :version_or_higher => '99.0.0') end - test.assert requires_redmine_plugin(:other, :version => other_version) test.assert requires_redmine_plugin(:other, :version => [other_version, '99.0.0']) test.assert_raise Redmine::PluginRequirementError do @@ -170,7 +171,6 @@ test.assert_raise Redmine::PluginNotFound do requires_redmine_plugin(:missing, :version => '0.1.0') end - end end end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/safe_attributes_test.rb --- a/test/unit/lib/redmine/safe_attributes_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/safe_attributes_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class BazaarAdapterTest < ActiveSupport::TestCase REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class CvsAdapterTest < ActiveSupport::TestCase REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s @@ -79,6 +79,22 @@ assert_equal "UTF-8", adpt2.path_encoding end + def test_root_url_path + to_test = { + ':pserver:cvs_user:cvs_password@123.456.789.123:9876/repo' => '/repo', + ':pserver:cvs_user:cvs_password@123.456.789.123/repo' => '/repo', + ':pserver:cvs_user:cvs_password@cvs_server:/repo' => '/repo', + ':pserver:cvs_user:cvs_password@cvs_server:9876/repo' => '/repo', + ':pserver:cvs_user:cvs_password@cvs_server/repo' => '/repo', + ':pserver:cvs_user:cvs_password@cvs_server/path/repo' => '/path/repo', + ':ext:cvsservername:/path' => '/path' + } + + to_test.each do |string, expected| + assert_equal expected, Redmine::Scm::Adapters::CvsAdapter.new('foo', string).send(:root_url_path), "#{string} failed" + end + end + private def test_scm_version_for(scm_command_version, version) diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class DarcsAdapterTest < ActiveSupport::TestCase REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/git_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class GitAdapterTest < ActiveSupport::TestCase REPOSITORY_PATH = Rails.root.join('tmp/test/git_repository').to_s @@ -61,8 +61,10 @@ ) assert @adapter @char_1 = CHAR_1_HEX.dup + @str_felix_hex = FELIX_HEX.dup if @char_1.respond_to?(:force_encoding) @char_1.force_encoding('UTF-8') + @str_felix_hex.force_encoding('ASCII-8BIT') end end @@ -390,22 +392,18 @@ assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", last_rev.scmid assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", last_rev.identifier assert_equal "Adam Soltys ", last_rev.author - assert_equal "2009-06-24 05:27:38".to_time, last_rev.time + assert_equal Time.gm(2009, 6, 24, 5, 27, 38), last_rev.time end def test_last_rev_with_spaces_in_filename last_rev = @adapter.lastrev("filemane with spaces.txt", "ed5bb786bbda2dee66a2d50faf51429dbc043a7b") - str_felix_hex = FELIX_HEX.dup last_rev_author = last_rev.author - if last_rev_author.respond_to?(:force_encoding) - str_felix_hex.force_encoding('ASCII-8BIT') - end assert_equal "ed5bb786bbda2dee66a2d50faf51429dbc043a7b", last_rev.scmid assert_equal "ed5bb786bbda2dee66a2d50faf51429dbc043a7b", last_rev.identifier - assert_equal "#{str_felix_hex} ", + assert_equal "#{@str_felix_hex} ", last_rev.author - assert_equal "2010-09-18 19:59:46".to_time, last_rev.time + assert_equal Time.gm(2010, 9, 18, 19, 59, 46), last_rev.time end def test_latin_1_path @@ -426,6 +424,19 @@ end end + def test_latin_1_user_annotate + ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', '83ca5fd546063a'].each do |r1| + annotate = @adapter.annotate(" filename with a leading space.txt ", r1) + assert_kind_of Redmine::Scm::Adapters::Annotate, annotate + assert_equal 1, annotate.lines.size + assert_equal "And this is a file with a leading and trailing space...", + annotate.lines[0].strip + assert_equal "83ca5fd546063a3c7dc2e568ba3355661a9e2b2c", + annotate.revisions[0].identifier + assert_equal @str_felix_hex, annotate.revisions[0].author + end + end + def test_entries_tag entries1 = @adapter.entries(nil, 'tag01.annotated', options = {:report_last_commit => true}) diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class MercurialAdapterTest < ActiveSupport::TestCase HELPERS_DIR = Redmine::Scm::Adapters::MercurialAdapter::HELPERS_DIR @@ -89,8 +89,8 @@ adp = Redmine::Scm::Adapters::MercurialAdapter.new(repo) repo_path = adp.info.root_url.gsub(/\\/, "/") assert_equal REPOSITORY_PATH, repo_path - assert_equal '31', adp.info.lastrev.revision - assert_equal '31eeee7395c8',adp.info.lastrev.scmid + assert_equal '33', adp.info.lastrev.revision + assert_equal '2e6d546429230f377d7d19c2078abd2dd909f235',adp.info.lastrev.scmid end end @@ -98,14 +98,14 @@ revisions = @adapter.revisions(nil, 2, 4) assert_equal 3, revisions.size assert_equal '2', revisions[0].revision - assert_equal '400bb8672109', revisions[0].scmid + assert_equal '400bb86721098697c7d17b3724c794c57636de70', revisions[0].scmid assert_equal '4', revisions[2].revision - assert_equal 'def6d2f1254a', revisions[2].scmid + assert_equal 'def6d2f1254a56fb8fbe9ec3b5c0451674dbd8b8', revisions[2].scmid revisions = @adapter.revisions(nil, 2, 4, {:limit => 2}) assert_equal 2, revisions.size assert_equal '2', revisions[0].revision - assert_equal '400bb8672109', revisions[0].scmid + assert_equal '400bb86721098697c7d17b3724c794c57636de70', revisions[0].scmid end def test_parents @@ -115,12 +115,12 @@ revs2 = @adapter.revisions(nil, 1, 1) assert_equal 1, revs2.size assert_equal 1, revs2[0].parents.size - assert_equal "0885933ad4f6", revs2[0].parents[0] + assert_equal "0885933ad4f68d77c2649cd11f8311276e7ef7ce", revs2[0].parents[0] revs3 = @adapter.revisions(nil, 30, 30) assert_equal 1, revs3.size assert_equal 2, revs3[0].parents.size - assert_equal "a94b0528f24f", revs3[0].parents[0] - assert_equal "3a330eb32958", revs3[0].parents[1] + assert_equal "a94b0528f24fe05ebaef496ae0500bb050772e36", revs3[0].parents[0] + assert_equal "3a330eb329586ea2adb3f83237c23310e744ebe9", revs3[0].parents[1] end def test_diff @@ -213,7 +213,7 @@ assert_equal 'file', readme.kind assert_equal 27, readme.size assert_equal '1', readme.lastrev.revision - assert_equal '9d5b5b004199', readme.lastrev.identifier + assert_equal '9d5b5b00419901478496242e0768deba1ce8c51e', readme.lastrev.identifier # 2007-12-14 10:24:01 +0100 assert_equal Time.gm(2007, 12, 14, 9, 24, 1), readme.lastrev.time @@ -242,7 +242,7 @@ assert_equal 'file', readme.kind assert_equal 21, readme.size assert_equal '0', readme.lastrev.revision - assert_equal '0885933ad4f6', readme.lastrev.identifier + assert_equal '0885933ad4f68d77c2649cd11f8311276e7ef7ce', readme.lastrev.identifier # 2007-12-14 10:22:52 +0100 assert_equal Time.gm(2007, 12, 14, 9, 22, 52), readme.lastrev.time end @@ -260,11 +260,52 @@ assert_equal 'file', readme.kind assert_equal 365, readme.size assert_equal '8', readme.lastrev.revision - assert_equal 'c51f5bb613cd', readme.lastrev.identifier + assert_equal 'c51f5bb613cd60793c2a9fe9df29332e74bb949f', readme.lastrev.identifier # 2001-02-01 00:00:00 -0900 assert_equal Time.gm(2001, 2, 1, 9, 0, 0), readme.lastrev.time end + def test_entry + entry = @adapter.entry() + assert_equal "", entry.path + assert_equal "dir", entry.kind + entry = @adapter.entry('') + assert_equal "", entry.path + assert_equal "dir", entry.kind + assert_nil @adapter.entry('invalid') + assert_nil @adapter.entry('/invalid') + assert_nil @adapter.entry('/invalid/') + assert_nil @adapter.entry('invalid/invalid') + assert_nil @adapter.entry('invalid/invalid/') + assert_nil @adapter.entry('/invalid/invalid') + assert_nil @adapter.entry('/invalid/invalid/') + ["README", "/README"].each do |path| + ["0", "0885933ad4f6", "0885933ad4f68d77c2649cd11f8311276e7ef7ce"].each do |rev| + entry = @adapter.entry(path, rev) + assert_equal "README", entry.path + assert_equal "file", entry.kind + assert_equal '0', entry.lastrev.revision + assert_equal '0885933ad4f68d77c2649cd11f8311276e7ef7ce', entry.lastrev.identifier + end + end + ["sources", "/sources", "/sources/"].each do |path| + ["0", "0885933ad4f6", "0885933ad4f68d77c2649cd11f8311276e7ef7ce"].each do |rev| + entry = @adapter.entry(path, rev) + assert_equal "sources", entry.path + assert_equal "dir", entry.kind + end + end + ["sources/watchers_controller.rb", "/sources/watchers_controller.rb"].each do |path| + ["0", "0885933ad4f6", "0885933ad4f68d77c2649cd11f8311276e7ef7ce"].each do |rev| + entry = @adapter.entry(path, rev) + assert_equal "sources/watchers_controller.rb", entry.path + assert_equal "file", entry.kind + assert_equal '0', entry.lastrev.revision + assert_equal '0885933ad4f68d77c2649cd11f8311276e7ef7ce', entry.lastrev.identifier + end + end + end + def test_locate_on_outdated_repository assert_equal 1, @adapter.entries("images", 0).size assert_equal 2, @adapter.entries("images").size @@ -288,9 +329,9 @@ def test_tagmap tm = { - @tag_char_1 => 'adf805632193', - 'tag_test.00' => '6987191f453a', - 'tag-init-revision' => '0885933ad4f6', + @tag_char_1 => 'adf805632193500ad3b615cd04f58f9b0769f576', + 'tag_test.00' => '6987191f453a5f6557018d522feea2c450d5588d', + 'tag-init-revision' => '0885933ad4f68d77c2649cd11f8311276e7ef7ce', } assert_equal tm, @adapter.tagmap end @@ -303,36 +344,36 @@ assert_equal 7, brs.length assert_equal 'default', brs[0].to_s assert_equal '31', brs[0].revision - assert_equal '31eeee7395c8', brs[0].scmid + assert_equal '31eeee7395c8c78e66dd54c50addd078d10b2355', brs[0].scmid assert_equal 'test-branch-01', brs[1].to_s assert_equal '30', brs[1].revision - assert_equal 'ad4dc4f80284', brs[1].scmid + assert_equal 'ad4dc4f80284a4f9168b77e0b6de288e5d207ee7', brs[1].scmid assert_equal @branch_char_1, brs[2].to_s assert_equal '27', brs[2].revision - assert_equal '7bbf4c738e71', brs[2].scmid + assert_equal '7bbf4c738e7145149d2e5eb1eed1d3a8ddd3b914', brs[2].scmid assert_equal 'branch (1)[2]&,%.-3_4', brs[3].to_s assert_equal '25', brs[3].revision - assert_equal 'afc61e85bde7', brs[3].scmid + assert_equal 'afc61e85bde74de930e5846c8451bd55b5bafc9c', brs[3].scmid assert_equal @branch_char_0, brs[4].to_s assert_equal '23', brs[4].revision - assert_equal 'c8d3e4887474', brs[4].scmid + assert_equal 'c8d3e4887474af6a589190140508037ebaa9d9c3', brs[4].scmid assert_equal 'test_branch.latin-1', brs[5].to_s assert_equal '22', brs[5].revision - assert_equal 'c2ffe7da686a', brs[5].scmid + assert_equal 'c2ffe7da686aa3d956e59f2a2854cf8980a8b768', brs[5].scmid assert_equal 'test-branch-00', brs[6].to_s assert_equal '13', brs[6].revision - assert_equal '3a330eb32958', brs[6].scmid + assert_equal '3a330eb329586ea2adb3f83237c23310e744ebe9', brs[6].scmid end def test_branchmap bm = { - 'default' => '31eeee7395c8', - 'test_branch.latin-1' => 'c2ffe7da686a', - 'branch (1)[2]&,%.-3_4' => 'afc61e85bde7', - 'test-branch-00' => '3a330eb32958', - "test-branch-01" => 'ad4dc4f80284', - @branch_char_0 => 'c8d3e4887474', - @branch_char_1 => '7bbf4c738e71', + 'default' => '31eeee7395c8c78e66dd54c50addd078d10b2355', + 'test_branch.latin-1' => 'c2ffe7da686aa3d956e59f2a2854cf8980a8b768', + 'branch (1)[2]&,%.-3_4' => 'afc61e85bde74de930e5846c8451bd55b5bafc9c', + 'test-branch-00' => '3a330eb329586ea2adb3f83237c23310e744ebe9', + "test-branch-01" => 'ad4dc4f80284a4f9168b77e0b6de288e5d207ee7', + @branch_char_0 => 'c8d3e4887474af6a589190140508037ebaa9d9c3', + @branch_char_1 => '7bbf4c738e7145149d2e5eb1eed1d3a8ddd3b914', } assert_equal bm, @adapter.branchmap end @@ -378,18 +419,18 @@ when 'branch (1)[2]&,%.-3_4' if @adapter.class.client_version_above?([1, 6]) assert_equal 3, nib0.size - assert_equal nib0[0], 'afc61e85bde7' + assert_equal 'afc61e85bde74de930e5846c8451bd55b5bafc9c', nib0[0] nib2 = @adapter.nodes_in_branch(bra, :limit => 2) assert_equal 2, nib2.size - assert_equal nib2[1], '933ca60293d7' + assert_equal '933ca60293d78f7c7979dd123cc0c02431683575', nib2[1] end when @branch_char_1 if @adapter.class.client_version_above?([1, 6]) assert_equal 2, nib0.size - assert_equal nib0[1], '08ff3227303e' + assert_equal '08ff3227303ec0dfcc818efa8e9cc652fe81859f', nib0[1] nib2 = @adapter.nodes_in_branch(bra, :limit => 1) assert_equal 1, nib2.size - assert_equal nib2[0], '7bbf4c738e71' + assert_equal '7bbf4c738e7145149d2e5eb1eed1d3a8ddd3b914', nib2[0] end end end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,7 +18,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class SubversionAdapterTest < ActiveSupport::TestCase diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/themes_test.rb --- a/test/unit/lib/redmine/themes_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/themes_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/unified_diff_test.rb --- a/test/unit/lib/redmine/unified_diff_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/unified_diff_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -221,6 +221,141 @@ assert_equal "test02.txt", diff[0].file_name end + def test_utf8_ja + ja = " text_tip_issue_end_day: " + ja += "\xe3\x81\x93\xe3\x81\xae\xe6\x97\xa5\xe3\x81\xab\xe7\xb5\x82\xe4\xba\x86\xe3\x81\x99\xe3\x82\x8b\xe3\x82\xbf\xe3\x82\xb9\xe3\x82\xaf" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new(read_diff_fixture('issue-12641-ja.diff'), :type => 'inline') + assert_equal 1, diff.size + assert_equal 12, diff.first.size + assert_equal ja, diff.first[4].html_line_left + end + end + + def test_utf8_ru + ru = " other: "\xd0\xbe\xd0\xba\xd0\xbe\xd0\xbb\xd0\xbe %{count} \xd1\x87\xd0\xb0\xd1\x81\xd0\xb0"" + ru.force_encoding('UTF-8') if ru.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new(read_diff_fixture('issue-12641-ru.diff'), :type => 'inline') + assert_equal 1, diff.size + assert_equal 8, diff.first.size + assert_equal ru, diff.first[3].html_line_left + end + end + + def test_offset_range_ascii_1 + raw = <<-DIFF +--- a.txt 2013-04-05 14:19:39.000000000 +0900 ++++ b.txt 2013-04-05 14:19:51.000000000 +0900 +@@ -1,3 +1,3 @@ + aaaa +-abc ++abcd + bbbb +DIFF + diff = Redmine::UnifiedDiff.new(raw, :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal "abc", diff.first[1].html_line_left + assert_equal "abcd", diff.first[1].html_line_right + end + + def test_offset_range_ascii_2 + raw = <<-DIFF +--- a.txt 2013-04-05 14:19:39.000000000 +0900 ++++ b.txt 2013-04-05 14:19:51.000000000 +0900 +@@ -1,3 +1,3 @@ + aaaa +-abc ++zabc + bbbb +DIFF + diff = Redmine::UnifiedDiff.new(raw, :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal "abc", diff.first[1].html_line_left + assert_equal "zabc", diff.first[1].html_line_right + end + + def test_offset_range_japanese_1 + ja1 = "\xe6\x97\xa5\xe6\x9c\xac" + ja1.force_encoding('UTF-8') if ja1.respond_to?(:force_encoding) + ja2 = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" + ja2.force_encoding('UTF-8') if ja2.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new( + read_diff_fixture('issue-13644-1.diff'), :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal ja1, diff.first[1].html_line_left + assert_equal ja2, diff.first[1].html_line_right + end + end + + def test_offset_range_japanese_2 + ja1 = "\xe6\x97\xa5\xe6\x9c\xac" + ja1.force_encoding('UTF-8') if ja1.respond_to?(:force_encoding) + ja2 = "\xe3\x81\xab\xe3\x81\xa3\xe3\x81\xbd\xe3\x82\x93\xe6\x97\xa5\xe6\x9c\xac" + ja2.force_encoding('UTF-8') if ja2.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new( + read_diff_fixture('issue-13644-2.diff'), :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal ja1, diff.first[1].html_line_left + assert_equal ja2, diff.first[1].html_line_right + end + end + + def test_offset_range_japanese_3 + # UTF-8 The 1st byte differs. + ja1 = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xa8\x98" + ja1.force_encoding('UTF-8') if ja1.respond_to?(:force_encoding) + ja2 = "\xe6\x97\xa5\xe6\x9c\xac\xe5\xa8\x98" + ja2.force_encoding('UTF-8') if ja2.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new( + read_diff_fixture('issue-13644-3.diff'), :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal ja1, diff.first[1].html_line_left + assert_equal ja2, diff.first[1].html_line_right + end + end + + def test_offset_range_japanese_4 + # UTF-8 The 2nd byte differs. + ja1 = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xa8\x98" + ja1.force_encoding('UTF-8') if ja1.respond_to?(:force_encoding) + ja2 = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x98" + ja2.force_encoding('UTF-8') if ja2.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new( + read_diff_fixture('issue-13644-4.diff'), :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal ja1, diff.first[1].html_line_left + assert_equal ja2, diff.first[1].html_line_right + end + end + + def test_offset_range_japanese_5 + # UTF-8 The 2nd byte differs. + ja1 = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xa8\x98ok" + ja1.force_encoding('UTF-8') if ja1.respond_to?(:force_encoding) + ja2 = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x98ok" + ja2.force_encoding('UTF-8') if ja2.respond_to?(:force_encoding) + with_settings :repositories_encodings => '' do + diff = Redmine::UnifiedDiff.new( + read_diff_fixture('issue-13644-5.diff'), :type => 'sbs') + assert_equal 1, diff.size + assert_equal 3, diff.first.size + assert_equal ja1, diff.first[1].html_line_left + assert_equal ja2, diff.first[1].html_line_right + end + end + private def read_diff_fixture(filename) diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/utils/date_calculation.rb --- a/test/unit/lib/redmine/utils/date_calculation.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/utils/date_calculation.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/views/builders/json_test.rb --- a/test/unit/lib/redmine/views/builders/json_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/views/builders/json_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/views/builders/xml_test.rb --- a/test/unit/lib/redmine/views/builders/xml_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/views/builders/xml_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/wiki_formatting/macros_test.rb --- a/test/unit/lib/redmine/wiki_formatting/macros_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/wiki_formatting/macros_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -235,6 +235,22 @@ assert_select_in result, 'a.collapsible', :text => 'Hide example' end + def test_macro_collapse_should_not_break_toc + text = <<-RAW +{{toc}} + +h1. Title + +{{collapse(Show example, Hide example) +h2. Heading +}}" +RAW + + expected_toc = '' + + assert_include expected_toc, textilizable(text).gsub(/[\r\n]/, '') + end + def test_macro_child_pages expected = "

      \n" + "
    • Child 1\n" + @@ -286,18 +302,30 @@ end def test_macro_thumbnail - assert_equal '

      testfile.PNG

      ', - textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14)) + link = link_to('testfile.PNG'.html_safe, + "http://test.host/attachments/17", + :class => "thumbnail", + :title => "testfile.PNG") + assert_equal "

      #{link}

      ", + textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14)) end def test_macro_thumbnail_with_size - assert_equal '

      testfile.PNG

      ', - textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14)) + link = link_to('testfile.PNG'.html_safe, + "http://test.host/attachments/17", + :class => "thumbnail", + :title => "testfile.PNG") + assert_equal "

      #{link}

      ", + textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14)) end def test_macro_thumbnail_with_title - assert_equal '

      testfile.PNG

      ', - textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14)) + link = link_to('testfile.PNG'.html_safe, + "http://test.host/attachments/17", + :class => "thumbnail", + :title => "Cool image") + assert_equal "

      #{link}

      ", + textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14)) end def test_macro_thumbnail_with_invalid_filename_should_fail diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/wiki_formatting/markdown_formatter.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/lib/redmine/wiki_formatting/markdown_formatter.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,80 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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::WikiFormatting::MarkdownFormatterTest < ActionView::TestCase + if Object.const_defined?(:Redcarpet) + + def setup + @formatter = Redmine::WikiFormatting::Markdown::Formatter + end + + def test_syntax_error_in_image_reference_should_not_raise_exception + assert @formatter.new("!>[](foo.png)").to_html + end + + # re-using the formatter after getting above error crashes the + # ruby interpreter. This seems to be related to + # https://github.com/vmg/redcarpet/issues/318 + def test_should_not_crash_redcarpet_after_syntax_error + @formatter.new("!>[](foo.png)").to_html rescue nil + assert @formatter.new("![](foo.png)").to_html.present? + end + + def test_inline_style + assert_equal "

      foo

      ", @formatter.new("**foo**").to_html.strip + end + + def test_not_set_intra_emphasis + assert_equal "

      foo_bar_baz

      ", @formatter.new("foo_bar_baz").to_html.strip + end + + def test_wiki_links_should_be_preserved + text = 'This is a wiki link: [[Foo]]' + assert_include '[[Foo]]', @formatter.new(text).to_html + end + + def test_redmine_links_with_double_quotes_should_be_preserved + text = 'This is a redmine link: version:"1.0"' + assert_include 'version:"1.0"', @formatter.new(text).to_html + end + + def test_should_support_syntax_highligth + text = <<-STR +~~~ruby +def foo +end +~~~ +STR + assert_select_in @formatter.new(text).to_html, 'pre code.ruby.syntaxhl' do + assert_select 'span.keyword', :text => 'def' + end + end + + def test_external_links_should_have_external_css_class + text = 'This is a [link](http://example.net/)' + assert_equal '

      This is a link

      ', @formatter.new(text).to_html.strip + end + + def test_locals_links_should_not_have_external_css_class + text = 'This is a [link](/issues)' + assert_equal '

      This is a link

      ', @formatter.new(text).to_html.strip + end + + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb --- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -159,11 +159,11 @@ ) end - def test_acronyms + def test_abbreviations assert_html_output( - 'this is an acronym: GPL(General Public License)' => 'this is an acronym: GPL', - '2 letters JP(Jean-Philippe) acronym' => '2 letters JP acronym', - 'GPL(This is a double-quoted "title")' => 'GPL' + 'this is an abbreviation: GPL(General Public License)' => 'this is an abbreviation: GPL', + '2 letters JP(Jean-Philippe) abbreviation' => '2 letters JP abbreviation', + 'GPL(This is a double-quoted "title")' => 'GPL' ) end @@ -268,6 +268,42 @@ assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end + def test_tables_with_lists + raw = <<-RAW +This is a table with lists: + +|cell11|cell12| +|cell21|ordered list +# item +# item 2| +|cell31|unordered list +* item +* item 2| + +RAW + + expected = <<-EXPECTED +

      This is a table with lists:

      + + + + + + + + + + + + + + +
      cell11cell12
      cell21ordered list
      # item
      # item 2
      cell31unordered list
      * item
      * item 2
      +EXPECTED + + assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') + end + def test_textile_should_not_mangle_brackets assert_equal '

      [msg1][msg2]

      ', to_html('[msg1][msg2]') end diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine/wiki_formatting_test.rb --- a/test/unit/lib/redmine/wiki_formatting_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine/wiki_formatting_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -30,6 +30,10 @@ assert_equal Redmine::WikiFormatting::NullFormatter::Helper, Redmine::WikiFormatting.helper_for('') end + def test_formats_for_select + assert_include ['Textile', 'textile'], Redmine::WikiFormatting.formats_for_select + end + def test_should_link_urls_and_email_addresses raw = <<-DIFF This is a sample *text* with a link: http://www.redmine.org diff -r d98d22a98252 -r fb9a13467253 test/unit/lib/redmine_test.rb --- a/test/unit/lib/redmine_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/lib/redmine_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/mail_handler_test.rb --- a/test/unit/mail_handler_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/mail_handler_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -41,6 +41,7 @@ def test_add_issue ActionMailer::Base.deliveries.clear + lft1 = new_issue_lft # This email contains: 'Project: onlinestore' issue = submit_email('ticket_on_given_project.eml') assert issue.is_a?(Issue) @@ -58,7 +59,7 @@ assert_equal Version.find_by_name('Alpha'), issue.fixed_version assert_equal 2.5, issue.estimated_hours assert_equal 30, issue.done_ratio - assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] + assert_equal [issue.id, lft1, lft1 + 1], [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) @@ -204,6 +205,14 @@ assert_equal user, issue.assigned_to end + def test_add_issue_should_set_default_start_date + with_settings :default_issue_start_date_to_creation_date => '1' do + issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) + assert issue.is_a?(Issue) + assert_equal Date.today, issue.start_date + end + end + def test_add_issue_with_cc issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) assert issue.is_a?(Issue) @@ -264,6 +273,7 @@ end def test_add_issue_by_anonymous_user_on_private_project_without_permission_check + lft1 = new_issue_lft assert_no_difference 'User.count' do assert_difference 'Issue.count' do issue = submit_email( @@ -275,7 +285,7 @@ assert issue.is_a?(Issue) assert issue.author.anonymous? assert !issue.project.is_public? - assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] + assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] end end end @@ -304,25 +314,81 @@ 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' - ) + with_settings :default_issue_start_date_to_creation_date => '0' do + 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 + end + + def test_add_issue_with_invalid_project_should_be_assigned_to_default_project + issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email| + email.gsub!(/^Project:.+$/, 'Project: invalid') + end 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.') + assert_equal 'ecookbook', issue.project.identifier end def test_add_issue_with_localized_attributes @@ -447,6 +513,21 @@ assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest end + def test_multiple_inline_text_parts_should_be_appended_to_issue_description + issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) + assert_include 'first', issue.description + assert_include 'second', issue.description + assert_include 'third', issue.description + end + + def test_attachment_text_part_should_be_added_as_issue_attachment + issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) + assert_not_include 'Plain text attachment', issue.description + attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'} + assert_not_nil attachment + assert_include 'Plain text attachment', File.read(attachment.diskfile) + end + def test_add_issue_with_iso_8859_1_subject issue = submit_email( 'subject_as_iso-8859-1.eml', @@ -469,6 +550,23 @@ assert_equal ja, issue.subject end + def test_add_issue_with_korean_body + # Make sure mail bodies with a charset unknown to Ruby + # but known to the Mail gem 2.5.4 are handled correctly + kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4." + if !kr.respond_to?(:force_encoding) + puts "\nOn Ruby 1.8, skip Korean encoding mail body test" + else + kr.force_encoding('UTF-8') + issue = submit_email( + 'body_ks_c_5601-1987.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal kr, issue.description + end + end + def test_add_issue_with_no_subject_header issue = submit_email( 'no_subject_header.eml', @@ -577,7 +675,7 @@ end end end - journal = Journal.first(:order => 'id DESC') + journal = Journal.order('id DESC').first assert_equal Issue.find(2), journal.journalized assert_equal 1, journal.details.size @@ -646,73 +744,73 @@ assert_equal 'This is a html-only email.', issue.description end - context "truncate emails based on the Setting" do - context "with no setting" do - setup do - Setting.mail_handler_body_delimiters = '' - end + test "truncate emails with no setting should add the entire email into the issue" do + with_settings :mail_handler_body_delimiters => '' do + issue = submit_email('ticket_on_given_project.eml') + assert_issue_created(issue) + assert issue.description.include?('---') + assert issue.description.include?('This paragraph is after the delimiter') + end + end - should "add the entire email into the issue" do - issue = submit_email('ticket_on_given_project.eml') - assert_issue_created(issue) - assert issue.description.include?('---') - assert issue.description.include?('This paragraph is after the delimiter') - end + test "truncate emails with a single string should truncate the email at the delimiter for the issue" do + with_settings :mail_handler_body_delimiters => '---' do + issue = submit_email('ticket_on_given_project.eml') + assert_issue_created(issue) + assert issue.description.include?('This paragraph is before delimiters') + assert issue.description.include?('--- This line starts with a delimiter') + assert !issue.description.match(/^---$/) + assert !issue.description.include?('This paragraph is after the delimiter') end + end - context "with a single string" do - setup do - Setting.mail_handler_body_delimiters = '---' - end - should "truncate the email at the delimiter for the issue" do - issue = submit_email('ticket_on_given_project.eml') - assert_issue_created(issue) - assert issue.description.include?('This paragraph is before delimiters') - assert issue.description.include?('--- This line starts with a delimiter') - assert !issue.description.match(/^---$/) - assert !issue.description.include?('This paragraph is after the delimiter') - end + test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do + with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do + journal = submit_email('issue_update_with_quoted_reply_above.eml') + assert journal.is_a?(Journal) + assert journal.notes.include?('An update to the issue by the sender.') + assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) + assert !journal.notes.include?('Looks like the JSON api for projects was missed.') end + end - context "with a single quoted reply (e.g. reply to a Redmine email notification)" do - setup do - Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---' - end - should "truncate the email at the delimiter with the quoted reply symbols (>)" do - journal = submit_email('issue_update_with_quoted_reply_above.eml') - assert journal.is_a?(Journal) - assert journal.notes.include?('An update to the issue by the sender.') - assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) - assert !journal.notes.include?('Looks like the JSON api for projects was missed.') - end + test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do + with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do + journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml') + assert journal.is_a?(Journal) + assert journal.notes.include?('An update to the issue by the sender.') + assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) + assert !journal.notes.include?('Looks like the JSON api for projects was missed.') end + end - context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do - setup do - Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---' - end - should "truncate the email at the delimiter with the quoted reply symbols (>)" do - journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml') - assert journal.is_a?(Journal) - assert journal.notes.include?('An update to the issue by the sender.') - assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) - assert !journal.notes.include?('Looks like the JSON api for projects was missed.') - end + test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do + with_settings :mail_handler_body_delimiters => "---\nBREAK" do + issue = submit_email('ticket_on_given_project.eml') + assert_issue_created(issue) + assert issue.description.include?('This paragraph is before delimiters') + assert !issue.description.include?('BREAK') + assert !issue.description.include?('This paragraph is between delimiters') + assert !issue.description.match(/^---$/) + assert !issue.description.include?('This paragraph is after the delimiter') end + end - context "with multiple strings" do - setup do - Setting.mail_handler_body_delimiters = "---\nBREAK" - end - should "truncate the email at the first delimiter found (BREAK)" do - issue = submit_email('ticket_on_given_project.eml') - assert_issue_created(issue) - assert issue.description.include?('This paragraph is before delimiters') - assert !issue.description.include?('BREAK') - assert !issue.description.include?('This paragraph is between delimiters') - assert !issue.description.match(/^---$/) - assert !issue.description.include?('This paragraph is after the delimiter') - end + def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored + with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do + issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + assert_equal 0, issue.reload.attachments.size + end + end + + def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached + with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do + issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + assert_equal 1, issue.reload.attachments.size end end @@ -741,14 +839,7 @@ assert_equal expected[0], user.login assert_equal expected[1], user.firstname assert_equal expected[2], user.lastname - end - end - - def test_new_user_from_attributes_should_respect_minimum_password_length - with_settings :password_min_length => 15 do - user = MailHandler.new_user_from_attributes('jsmith@example.net') - assert user.valid? - assert user.password.length >= 15 + assert_equal 'only_my_events', user.mail_notification end end @@ -767,8 +858,7 @@ :unknown_user => 'create' ) end - - user = User.first(:order => 'id DESC') + user = User.order('id DESC').first assert_equal "foo@example.org", user.mail str1 = "\xc3\x84\xc3\xa4" str2 = "\xc3\x96\xc3\xb6" @@ -778,6 +868,19 @@ assert_equal str2, user.lastname end + def test_extract_options_from_env_should_return_options + options = MailHandler.extract_options_from_env({ + 'tracker' => 'defect', + 'project' => 'foo', + 'unknown_user' => 'create' + }) + + assert_equal({ + :issue => {:tracker => 'defect', :project => 'foo'}, + :unknown_user => 'create' + }, options) + end + private def submit_email(filename, options={}) diff -r d98d22a98252 -r fb9a13467253 test/unit/mailer_test.rb --- a/test/unit/mailer_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/mailer_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -42,7 +42,7 @@ Setting.protocol = 'https' journal = Journal.find(3) - assert Mailer.issue_edit(journal).deliver + assert Mailer.deliver_issue_edit(journal) mail = last_email assert_not_nil mail @@ -55,7 +55,7 @@ # link to a referenced ticket assert_select 'a[href=?][title=?]', 'https://mydomain.foo/issues/1', - 'Can't print recipes (New)', + "#{ESCAPED_UCANT} print recipes (New)", :text => '#1' # link to a changeset assert_select 'a[href=?][title=?]', @@ -81,7 +81,7 @@ Setting.protocol = 'http' journal = Journal.find(3) - assert Mailer.issue_edit(journal).deliver + assert Mailer.deliver_issue_edit(journal) mail = last_email assert_not_nil mail @@ -94,7 +94,7 @@ # link to a referenced ticket assert_select 'a[href=?][title=?]', 'http://mydomain.foo/rdm/issues/1', - 'Can't print recipes (New)', + "#{ESCAPED_UCANT} print recipes (New)", :text => '#1' # link to a changeset assert_select 'a[href=?][title=?]', @@ -113,6 +113,16 @@ end end + def test_issue_edit_should_generate_url_with_hostname_for_relations + journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now) + journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2) + Mailer.deliver_issue_edit(journal) + assert_not_nil last_email + assert_select_email do + assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2' + end + end + def test_generated_links_with_prefix_and_no_relative_url_root Setting.default_language = 'en' relative_url_root = Redmine::Utils.relative_url_root @@ -121,7 +131,7 @@ Redmine::Utils.relative_url_root = nil journal = Journal.find(3) - assert Mailer.issue_edit(journal).deliver + assert Mailer.deliver_issue_edit(journal) mail = last_email assert_not_nil mail @@ -134,7 +144,7 @@ # link to a referenced ticket assert_select 'a[href=?][title=?]', 'http://mydomain.foo/rdm/issues/1', - 'Can't print recipes (New)', + "#{ESCAPED_UCANT} print recipes (New)", :text => '#1' # link to a changeset assert_select 'a[href=?][title=?]', @@ -158,7 +168,7 @@ def test_email_headers issue = Issue.find(1) - Mailer.issue_add(issue).deliver + Mailer.deliver_issue_add(issue) mail = last_email assert_not_nil mail assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s @@ -168,7 +178,7 @@ def test_email_headers_should_include_sender issue = Issue.find(1) - Mailer.issue_add(issue).deliver + Mailer.deliver_issue_add(issue) mail = last_email assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s end @@ -176,7 +186,7 @@ def test_plain_text_mail Setting.plain_text_mail = 1 journal = Journal.find(2) - Mailer.issue_edit(journal).deliver + Mailer.deliver_issue_edit(journal) mail = last_email assert_equal "text/plain; charset=UTF-8", mail.content_type assert_equal 0, mail.parts.size @@ -186,7 +196,7 @@ def test_html_mail Setting.plain_text_mail = 0 journal = Journal.find(2) - Mailer.issue_edit(journal).deliver + Mailer.deliver_issue_edit(journal) mail = last_email assert_equal 2, mail.parts.size assert mail.encoded.include?('href') @@ -210,19 +220,19 @@ end def test_should_not_send_email_without_recipient - news = News.find(:first) + news = News.first user = news.author # Remove members except news author news.project.memberships.each {|m| m.destroy unless m.user == user} - user.pref[:no_self_notified] = false + user.pref.no_self_notified = false user.pref.save User.current = user Mailer.news_added(news.reload).deliver assert_equal 1, last_email.bcc.size # nobody to notify - user.pref[:no_self_notified] = true + user.pref.no_self_notified = true user.pref.save User.current = user ActionMailer::Base.deliveries.clear @@ -231,19 +241,21 @@ end def test_issue_add_message_id - issue = Issue.find(1) - Mailer.issue_add(issue).deliver + issue = Issue.find(2) + Mailer.deliver_issue_add(issue) mail = last_email - assert_equal Mailer.message_id_for(issue), mail.message_id - assert_nil mail.references + assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.issue-2.20060719190421@example.net", mail.references end def test_issue_edit_message_id - journal = Journal.find(1) - Mailer.issue_edit(journal).deliver + journal = Journal.find(3) + journal.issue = Issue.find(2) + + Mailer.deliver_issue_edit(journal) mail = last_email - assert_equal Mailer.message_id_for(journal), mail.message_id - assert_include Mailer.message_id_for(journal.issue), mail.references + assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.issue-2.20060719190421@example.net", mail.references assert_select_email do # link to the update assert_select "a[href=?]", @@ -255,8 +267,8 @@ message = Message.find(1) Mailer.message_posted(message).deliver mail = last_email - assert_equal Mailer.message_id_for(message), mail.message_id - assert_nil mail.references + assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.message-1.20070512151532@example.net", mail.references assert_select_email do # link to the message assert_select "a[href=?]", @@ -269,8 +281,8 @@ message = Message.find(3) Mailer.message_posted(message).deliver mail = last_email - assert_equal Mailer.message_id_for(message), mail.message_id - assert_include Mailer.message_id_for(message.parent), mail.references + assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id + assert_include "redmine.message-1.20070512151532@example.net", mail.references assert_select_email do # link to the reply assert_select "a[href=?]", @@ -279,43 +291,62 @@ end end - context("#issue_add") do - setup do - ActionMailer::Base.deliveries.clear - Setting.bcc_recipients = '1' - @issue = Issue.find(1) + test "#issue_add should notify project members" do + issue = Issue.find(1) + assert Mailer.deliver_issue_add(issue) + assert last_email.bcc.include?('dlopper@somenet.foo') + end + + test "#issue_add should not notify project members that are not allow to view the issue" do + issue = Issue.find(1) + Role.find(2).remove_permission!(:view_issues) + assert Mailer.deliver_issue_add(issue) + assert !last_email.bcc.include?('dlopper@somenet.foo') + end + + test "#issue_add should notify issue watchers" do + issue = Issue.find(1) + user = User.find(9) + # minimal email notification options + user.pref.no_self_notified = '1' + user.pref.save + user.mail_notification = false + user.save + + Watcher.create!(:watchable => issue, :user => user) + assert Mailer.deliver_issue_add(issue) + assert last_email.bcc.include?(user.mail) + end + + test "#issue_add should not notify watchers not allowed to view the issue" do + issue = Issue.find(1) + user = User.find(9) + Watcher.create!(:watchable => issue, :user => user) + Role.non_member.remove_permission!(:view_issues) + assert Mailer.deliver_issue_add(issue) + assert !last_email.bcc.include?(user.mail) + end + + def test_issue_add_should_include_enabled_fields + Setting.default_language = 'en' + issue = Issue.find(2) + assert Mailer.deliver_issue_add(issue) + assert_mail_body_match '* Target version: 1.0', last_email + assert_select_email do + assert_select 'li', :text => 'Target version: 1.0' end + end - should "notify project members" do - assert Mailer.issue_add(@issue).deliver - assert last_email.bcc.include?('dlopper@somenet.foo') - end - - should "not notify project members that are not allow to view the issue" do - Role.find(2).remove_permission!(:view_issues) - assert Mailer.issue_add(@issue).deliver - assert !last_email.bcc.include?('dlopper@somenet.foo') - end - - should "notify issue watchers" do - user = User.find(9) - # minimal email notification options - user.pref[:no_self_notified] = '1' - user.pref.save - user.mail_notification = false - user.save - - Watcher.create!(:watchable => @issue, :user => user) - assert Mailer.issue_add(@issue).deliver - assert last_email.bcc.include?(user.mail) - end - - should "not notify watchers not allowed to view the issue" do - user = User.find(9) - Watcher.create!(:watchable => @issue, :user => user) - Role.non_member.remove_permission!(:view_issues) - assert Mailer.issue_add(@issue).deliver - assert !last_email.bcc.include?(user.mail) + def test_issue_add_should_not_include_disabled_fields + Setting.default_language = 'en' + issue = Issue.find(2) + tracker = issue.tracker + tracker.core_fields -= ['fixed_version_id'] + tracker.save! + assert Mailer.deliver_issue_add(issue) + assert_mail_body_no_match 'Target version', last_email + assert_select_email do + assert_select 'li', :text => /Target version/, :count => 0 end end @@ -324,7 +355,7 @@ issue = Issue.find(1) valid_languages.each do |lang| Setting.default_language = lang.to_s - assert Mailer.issue_add(issue).deliver + assert Mailer.deliver_issue_add(issue) end end @@ -332,7 +363,7 @@ journal = Journal.find(1) valid_languages.each do |lang| Setting.default_language = lang.to_s - assert Mailer.issue_edit(journal).deliver + assert Mailer.deliver_issue_edit(journal) end end @@ -342,11 +373,11 @@ journal.save! Role.find(2).add_permission! :view_private_notes - Mailer.issue_edit(journal).deliver + Mailer.deliver_issue_edit(journal) assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort Role.find(2).remove_permission! :view_private_notes - Mailer.issue_edit(journal).deliver + Mailer.deliver_issue_edit(journal) assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort end @@ -357,14 +388,41 @@ journal.save! Role.non_member.add_permission! :view_private_notes - Mailer.issue_edit(journal).deliver + Mailer.deliver_issue_edit(journal) assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort Role.non_member.remove_permission! :view_private_notes - Mailer.issue_edit(journal).deliver + Mailer.deliver_issue_edit(journal) assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort end + def test_issue_edit_should_mark_private_notes + journal = Journal.find(2) + journal.private_notes = true + journal.save! + + with_settings :default_language => 'en' do + Mailer.deliver_issue_edit(journal) + end + assert_mail_body_match '(Private notes)', last_email + end + + def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue + issue = Issue.generate! + private_issue = Issue.generate!(:is_private => true) + IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates') + issue.reload + assert_equal 1, issue.journals.size + journal = issue.journals.first + ActionMailer::Base.deliveries.clear + + Mailer.deliver_issue_edit(journal) + last_email.bcc.each do |email| + user = User.find_by_mail(email) + assert private_issue.visible?(user), "Issue was not visible to #{user}" + end + end + def test_document_added document = Document.find(1) valid_languages.each do |lang| @@ -402,13 +460,24 @@ end def test_news_added - news = News.find(:first) + news = News.first valid_languages.each do |lang| Setting.default_language = lang.to_s assert Mailer.news_added(news).deliver end end + def test_news_added_should_notify_project_news_watchers + user1 = User.generate! + user2 = User.generate! + news = News.find(1) + news.project.enabled_module('news').add_watcher(user1) + + Mailer.news_added(news).deliver + assert_include user1.mail, last_email.bcc + assert_not_include user2.mail, last_email.bcc + end + def test_news_comment_added comment = Comment.find(2) valid_languages.each do |lang| @@ -418,7 +487,7 @@ end def test_message_posted - message = Message.find(:first) + message = Message.first recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author} recipients = recipients.compact.uniq valid_languages.each do |lang| @@ -581,14 +650,74 @@ assert ActionMailer::Base.perform_deliveries end + def test_token_for_should_strip_trailing_gt_from_address_with_full_name + with_settings :mail_from => "Redmine Mailer" do + assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!) + end + end + def test_layout_should_include_the_emails_header with_settings :emails_header => "*Header content*" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".header" do + assert_select "strong", :text => "Header content" + end + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_include "*Header content*", mail.body.decoded + end + end + end + + def test_layout_should_not_include_empty_emails_header + with_settings :emails_header => "", :plain_text_mail => 0 do assert Mailer.test_email(User.find(1)).deliver assert_select_email do - assert_select ".header" do - assert_select "strong", :text => "Header content" + assert_select ".header", false + end + end + end + + def test_layout_should_include_the_emails_footer + with_settings :emails_footer => "*Footer content*" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".footer" do + assert_select "strong", :text => "Footer content" + end end end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_include "\n-- \n", mail.body.decoded + assert_include "*Footer content*", mail.body.decoded + end + end + end + + def test_layout_should_not_include_empty_emails_footer + with_settings :emails_footer => "" do + with_settings :plain_text_mail => 0 do + assert Mailer.test_email(User.find(1)).deliver + assert_select_email do + assert_select ".footer", false + end + end + with_settings :plain_text_mail => 1 do + assert Mailer.test_email(User.find(1)).deliver + mail = last_email + assert_not_nil mail + assert_not_include "\n-- \n", mail.body.decoded + end end end @@ -600,6 +729,33 @@ assert_include '<tag>', html_part.body.encoded end + def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true + mail = Mailer.test_email(User.find(1)) + mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) + + ActionMailer::Base.raise_delivery_errors = true + assert_raise Exception, "delivery error" do + mail.deliver + end + ensure + ActionMailer::Base.raise_delivery_errors = false + end + + def test_should_log_delivery_errors_when_raise_delivery_errors_is_false + mail = Mailer.test_email(User.find(1)) + mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error")) + + Rails.logger.expects(:error).with("Email delivery error: delivery error") + ActionMailer::Base.raise_delivery_errors = false + assert_nothing_raised do + mail.deliver + end + end + + def test_mail_should_return_a_mail_message + assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1)) + end + private def last_email diff -r d98d22a98252 -r fb9a13467253 test/unit/member_test.rb --- a/test/unit/member_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/member_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -25,7 +25,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :groups_users, :watchers, :journals, :journal_details, @@ -70,7 +69,8 @@ assert !member.save # must have one role at least - user = User.new(:firstname => "new1", :lastname => "user1", :mail => "test_validate@somenet.foo") + user = User.new(:firstname => "new1", :lastname => "user1", + :mail => "test_validate@somenet.foo") user.login = "test_validate" user.password, user.password_confirmation = "password", "password" assert user.save @@ -85,7 +85,8 @@ end def test_validate_member_role - user = User.new(:firstname => "new1", :lastname => "user1", :mail => "test_validate@somenet.foo") + user = User.new(:firstname => "new1", :lastname => "user1", + :mail => "test_validate@somenet.foo") user.login = "test_validate_member_role" user.password, user.password_confirmation = "password", "password" assert user.save @@ -106,6 +107,23 @@ assert_nil category1.assigned_to_id end + def test_destroy_should_trigger_callbacks_only_once + Member.class_eval { def destroy_test_callback; end} + Member.after_destroy :destroy_test_callback + + m = Member.create!(:user_id => 1, :project_id => 1, :role_ids => [1,3]) + + Member.any_instance.expects(:destroy_test_callback).once + assert_difference 'Member.count', -1 do + assert_difference 'MemberRole.count', -2 do + m.destroy + end + end + assert m.destroyed? + ensure + Member._destroy_callbacks.reject! {|c| c.filter==:destroy_test_callback} + end + def test_sort_without_roles a = Member.new(:roles => [Role.first]) b = Member.new @@ -122,70 +140,4 @@ assert_equal -1, a <=> b assert_equal 1, b <=> a end - - context "removing permissions" do - setup do - Watcher.delete_all("user_id = 9") - user = User.find(9) - # public - Watcher.create!(:watchable => Issue.find(1), :user => user) - # private - Watcher.create!(:watchable => Issue.find(4), :user => user) - Watcher.create!(:watchable => Message.find(7), :user => user) - Watcher.create!(:watchable => Wiki.find(2), :user => user) - Watcher.create!(:watchable => WikiPage.find(3), :user => user) - end - - context "of user" do - setup do - @member = Member.create!(:project => Project.find(2), :principal => User.find(9), :role_ids => [1, 2]) - end - - context "by deleting membership" do - should "prune watchers" do - assert_difference 'Watcher.count', -4 do - @member.destroy - end - end - end - - context "by updating roles" do - should "prune watchers" do - Role.find(2).remove_permission! :view_wiki_pages - member = Member.first(:order => 'id desc') - assert_difference 'Watcher.count', -2 do - member.role_ids = [2] - member.save - end - assert !Message.find(7).watched_by?(@user) - end - end - end - - context "of group" do - setup do - group = Group.find(10) - @member = Member.create!(:project => Project.find(2), :principal => group, :role_ids => [1, 2]) - group.users << User.find(9) - end - - context "by deleting membership" do - should "prune watchers" do - assert_difference 'Watcher.count', -4 do - @member.destroy - end - end - end - - context "by updating roles" do - should "prune watchers" do - Role.find(2).remove_permission! :view_wiki_pages - assert_difference 'Watcher.count', -2 do - @member.role_ids = [2] - @member.save - end - end - end - end - end end diff -r d98d22a98252 -r fb9a13467253 test/unit/message_test.rb --- a/test/unit/message_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/message_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -36,9 +36,9 @@ assert message.save @board.reload # topics count incremented - assert_equal topics_count+1, @board[:topics_count] + assert_equal topics_count + 1, @board[:topics_count] # messages count incremented - assert_equal messages_count+1, @board[:messages_count] + assert_equal messages_count + 1, @board[:messages_count] assert_equal message, @board.last_message # author should be watching the message assert message.watched_by?(@user) @@ -47,13 +47,13 @@ def test_reply topics_count = @board.topics_count messages_count = @board.messages_count - @message = Message.find(1) - replies_count = @message.replies_count + message = Message.find(1) + replies_count = message.replies_count reply_author = User.find(2) reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', - :parent => @message, :author => reply_author) + :parent => message, :author => reply_author) assert reply.save @board.reload # same topics count @@ -61,42 +61,42 @@ # messages count incremented assert_equal messages_count+1, @board[:messages_count] assert_equal reply, @board.last_message - @message.reload + message.reload # replies count incremented - assert_equal replies_count+1, @message[:replies_count] - assert_equal reply, @message.last_reply + assert_equal replies_count+1, message[:replies_count] + assert_equal reply, message.last_reply # author should be watching the message - assert @message.watched_by?(reply_author) + assert message.watched_by?(reply_author) end def test_cannot_reply_to_locked_topic topics_count = @board.topics_count messages_count = @board.messages_count - @message = Message.find(1) - replies_count = @message.replies_count - assert_equal false, @message.locked - @message.locked = true - assert @message.save - assert_equal true, @message.locked + message = Message.find(1) + replies_count = message.replies_count + assert_equal false, message.locked + message.locked = true + assert message.save + assert_equal true, message.locked reply_author = User.find(2) reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', - :parent => @message, :author => reply_author) + :parent => message, :author => reply_author) reply.save assert_equal 1, reply.errors.count end def test_moving_message_should_update_counters - @message = Message.find(1) + message = Message.find(1) assert_no_difference 'Message.count' do # Previous board assert_difference 'Board.find(1).topics_count', -1 do - assert_difference 'Board.find(1).messages_count', -(1 + @message.replies_count) do + assert_difference 'Board.find(1).messages_count', -(1 + message.replies_count) do # New board assert_difference 'Board.find(2).topics_count' do - assert_difference 'Board.find(2).messages_count', (1 + @message.replies_count) do - @message.update_attributes(:board_id => 2) + assert_difference 'Board.find(2).messages_count', (1 + message.replies_count) do + message.update_attributes(:board_id => 2) end end end @@ -115,7 +115,7 @@ board.reload # Replies deleted - assert Message.find_all_by_parent_id(1).empty? + assert Message.where(:parent_id => 1).empty? # Checks counters assert_equal topics_count - 1, board.topics_count assert_equal messages_count - 3, board.messages_count diff -r d98d22a98252 -r fb9a13467253 test/unit/news_test.rb --- a/test/unit/news_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/news_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -21,7 +21,7 @@ fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news def valid_news - { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) } + { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.first } end def setup diff -r d98d22a98252 -r fb9a13467253 test/unit/principal_test.rb --- a/test/unit/principal_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/principal_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -31,8 +31,10 @@ end def test_member_of_scope_should_return_the_union_of_all_members - projects = Project.find_all_by_id(1, 2) - assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort + projects = Project.find([1]) + assert_equal [3, 2], Principal.member_of(projects).sort.map(&:id) + projects = Project.find([1, 2]) + assert_equal [3, 2, 8, 11], Principal.member_of(projects).sort.map(&:id) end def test_member_of_scope_should_be_empty_for_no_projects @@ -40,73 +42,73 @@ end def test_not_member_of_scope_should_return_users_that_have_no_memberships - projects = Project.find_all_by_id(1, 2) - expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort - assert_equal expected, Principal.not_member_of(projects).sort + [[1], [1, 2]].each do |ids| + projects = Project.find(ids) + assert_equal ids.size, projects.count + expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort + assert_equal expected, Principal.not_member_of(projects).sort + end end def test_not_member_of_scope_should_be_empty_for_no_projects assert_equal [], Principal.not_member_of([]).sort end - context "#like" do - setup do - Principal.create!(:login => 'login') - Principal.create!(:login => 'login2') + def test_sorted_scope_should_sort_users_before_groups + scope = Principal.where("type <> ?", 'AnonymousUser') + expected_order = scope.all.sort do |a, b| + if a.is_a?(User) && b.is_a?(Group) + -1 + elsif a.is_a?(Group) && b.is_a?(User) + 1 + else + a.name.downcase <=> b.name.downcase + end + end + assert_equal expected_order.map(&:name).map(&:downcase), + scope.sorted.map(&:name).map(&:downcase) + end - Principal.create!(:firstname => 'firstname') - Principal.create!(:firstname => 'firstname2') + test "like scope should search login" do + results = Principal.like('jsmi') - Principal.create!(:lastname => 'lastname') - Principal.create!(:lastname => 'lastname2') + assert results.any? + assert results.all? {|u| u.login.match(/jsmi/i) } + end - Principal.create!(:mail => 'mail@example.com') - Principal.create!(:mail => 'mail2@example.com') + test "like scope should search firstname" do + results = Principal.like('john') - @palmer = Principal.create!(:firstname => 'David', :lastname => 'Palmer') - end + assert results.any? + assert results.all? {|u| u.firstname.match(/john/i) } + end - should "search login" do - results = Principal.like('login') + test "like scope should search lastname" do + results = Principal.like('smi') - assert_equal 2, results.count - assert results.all? {|u| u.login.match(/login/) } - end + assert results.any? + assert results.all? {|u| u.lastname.match(/smi/i) } + end - should "search firstname" do - results = Principal.like('firstname') + test "like scope should search mail" do + results = Principal.like('somenet') - assert_equal 2, results.count - assert results.all? {|u| u.firstname.match(/firstname/) } - end + assert results.any? + assert results.all? {|u| u.mail.match(/somenet/i) } + end - should "search lastname" do - results = Principal.like('lastname') + test "like scope should search firstname and lastname" do + results = Principal.like('john smi') - assert_equal 2, results.count - assert results.all? {|u| u.lastname.match(/lastname/) } - end + assert_equal 1, results.count + assert_equal User.find(2), results.first + end - should "search mail" do - results = Principal.like('mail') + test "like scope should search lastname and firstname" do + results = Principal.like('smith joh') - assert_equal 2, results.count - assert results.all? {|u| u.mail.match(/mail/) } - end - - should "search firstname and lastname" do - results = Principal.like('david palm') - - assert_equal 1, results.count - assert_equal @palmer, results.first - end - - should "search lastname and firstname" do - results = Principal.like('palmer davi') - - assert_equal 1, results.count - assert_equal @palmer, results.first - end + assert_equal 1, results.count + assert_equal User.find(2), results.first end def test_like_scope_with_cyrillic_name diff -r d98d22a98252 -r fb9a13467253 test/unit/project_copy_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/project_copy_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,337 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 ProjectCopyTest < ActiveSupport::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :journals, :journal_details, + :enumerations, :users, :issue_categories, + :projects_trackers, + :custom_fields, + :custom_fields_projects, + :custom_fields_trackers, + :custom_values, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, + :groups_users, + :boards, :messages, + :repositories, + :news, :comments, + :documents + + def setup + ProjectCustomField.destroy_all + @source_project = Project.find(2) + @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test') + @project.trackers = @source_project.trackers + @project.enabled_module_names = @source_project.enabled_modules.collect(&:name) + end + + test "#copy should copy issues" do + @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'), + :subject => "copy issue status", + :tracker_id => 1, + :assigned_to_id => 2, + :project_id => @source_project.id) + assert @project.valid? + assert @project.issues.empty? + assert @project.copy(@source_project) + + assert_equal @source_project.issues.size, @project.issues.size + @project.issues.each do |issue| + assert issue.valid? + assert ! issue.assigned_to.blank? + assert_equal @project, issue.project + end + + copied_issue = @project.issues.where(:subject => "copy issue status").first + assert copied_issue + assert copied_issue.status + assert_equal "Closed", copied_issue.status.name + end + + test "#copy should copy issues custom values" do + field = IssueCustomField.generate!(:is_for_all => true, :trackers => Tracker.all) + issue = Issue.generate!(:project => @source_project, :subject => 'Custom field copy') + issue.custom_field_values = {field.id => 'custom'} + issue.save! + assert_equal 'custom', issue.reload.custom_field_value(field) + + assert @project.copy(@source_project) + copy = @project.issues.find_by_subject('Custom field copy') + assert copy + assert_equal 'custom', copy.reload.custom_field_value(field) + end + + test "#copy should copy issues assigned to a locked version" do + User.current = User.find(1) + assigned_version = Version.generate!(:name => "Assigned Issues") + @source_project.versions << assigned_version + Issue.generate!(:project => @source_project, + :fixed_version_id => assigned_version.id, + :subject => "copy issues assigned to a locked version") + assigned_version.update_attribute :status, 'locked' + + assert @project.copy(@source_project) + @project.reload + copied_issue = @project.issues.where(:subject => "copy issues assigned to a locked version").first + + assert copied_issue + assert copied_issue.fixed_version + assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name + assert_equal 'locked', copied_issue.fixed_version.status + end + + test "#copy should change the new issues to use the copied version" do + User.current = User.find(1) + assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open') + @source_project.versions << assigned_version + assert_equal 3, @source_project.versions.size + Issue.generate!(:project => @source_project, + :fixed_version_id => assigned_version.id, + :subject => "change the new issues to use the copied version") + + assert @project.copy(@source_project) + @project.reload + copied_issue = @project.issues.where(:subject => "change the new issues to use the copied version").first + + assert copied_issue + assert copied_issue.fixed_version + assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name + assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record + end + + test "#copy should keep target shared versions from other project" do + assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system') + issue = Issue.generate!(:project => @source_project, + :fixed_version => assigned_version, + :subject => "keep target shared versions") + + assert @project.copy(@source_project) + @project.reload + copied_issue = @project.issues.where(:subject => "keep target shared versions").first + + assert copied_issue + assert_equal assigned_version, copied_issue.fixed_version + end + + test "#copy should copy issue relations" do + Setting.cross_project_issue_relations = '1' + + second_issue = Issue.generate!(:status_id => 5, + :subject => "copy issue relation", + :tracker_id => 1, + :assigned_to_id => 2, + :project_id => @source_project.id) + source_relation = IssueRelation.create!(:issue_from => Issue.find(4), + :issue_to => second_issue, + :relation_type => "relates") + source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1), + :issue_to => second_issue, + :relation_type => "duplicates") + + assert @project.copy(@source_project) + assert_equal @source_project.issues.count, @project.issues.count + copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4 + copied_second_issue = @project.issues.find_by_subject("copy issue relation") + + # First issue with a relation on project + assert_equal 1, copied_issue.relations.size, "Relation not copied" + copied_relation = copied_issue.relations.first + assert_equal "relates", copied_relation.relation_type + assert_equal copied_second_issue.id, copied_relation.issue_to_id + assert_not_equal source_relation.id, copied_relation.id + + # Second issue with a cross project relation + assert_equal 2, copied_second_issue.relations.size, "Relation not copied" + copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first + assert_equal "duplicates", copied_relation.relation_type + assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept" + assert_not_equal source_relation_cross_project.id, copied_relation.id + end + + test "#copy should copy issue attachments" do + issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id) + Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1) + @source_project.issues << issue + assert @project.copy(@source_project) + + copied_issue = @project.issues.where(:subject => "copy with attachment").first + assert_not_nil copied_issue + assert_equal 1, copied_issue.attachments.count, "Attachment not copied" + assert_equal "testfile.txt", copied_issue.attachments.first.filename + end + + test "#copy should copy memberships" do + assert @project.valid? + assert @project.members.empty? + assert @project.copy(@source_project) + + assert_equal @source_project.memberships.size, @project.memberships.size + @project.memberships.each do |membership| + assert membership + assert_equal @project, membership.project + end + end + + test "#copy should copy memberships with groups and additional roles" do + group = Group.create!(:lastname => "Copy group") + user = User.find(7) + group.users << user + # group role + Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2]) + member = Member.find_by_user_id_and_project_id(user.id, @source_project.id) + # additional role + member.role_ids = [1] + + assert @project.copy(@source_project) + member = Member.find_by_user_id_and_project_id(user.id, @project.id) + assert_not_nil member + assert_equal [1, 2], member.role_ids.sort + end + + test "#copy should copy project specific queries" do + assert @project.valid? + assert @project.queries.empty? + assert @project.copy(@source_project) + + assert_equal @source_project.queries.size, @project.queries.size + @project.queries.each do |query| + assert query + assert_equal @project, query.project + end + assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort + end + + test "#copy should copy versions" do + @source_project.versions << Version.generate! + @source_project.versions << Version.generate! + + assert @project.versions.empty? + assert @project.copy(@source_project) + + assert_equal @source_project.versions.size, @project.versions.size + @project.versions.each do |version| + assert version + assert_equal @project, version.project + end + end + + test "#copy should copy wiki" do + assert_difference 'Wiki.count' do + assert @project.copy(@source_project) + end + + assert @project.wiki + assert_not_equal @source_project.wiki, @project.wiki + assert_equal "Start page", @project.wiki.start_page + end + + test "#copy should copy wiki without wiki module" do + project = Project.new(:name => 'Copy Test', :identifier => 'copy-test', :enabled_module_names => []) + assert_difference 'Wiki.count' do + assert project.copy(@source_project) + end + + assert project.wiki + end + + test "#copy should copy wiki pages and content with hierarchy" do + assert_difference 'WikiPage.count', @source_project.wiki.pages.size do + assert @project.copy(@source_project) + end + + assert @project.wiki + assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size + + @project.wiki.pages.each do |wiki_page| + assert wiki_page.content + assert !@source_project.wiki.pages.include?(wiki_page) + end + + parent = @project.wiki.find_page('Parent_page') + child1 = @project.wiki.find_page('Child_page_1') + child2 = @project.wiki.find_page('Child_page_2') + assert_equal parent, child1.parent + assert_equal parent, child2.parent + end + + test "#copy should copy issue categories" do + assert @project.copy(@source_project) + + assert_equal 2, @project.issue_categories.size + @project.issue_categories.each do |issue_category| + assert !@source_project.issue_categories.include?(issue_category) + end + end + + test "#copy should copy boards" do + assert @project.copy(@source_project) + + assert_equal 1, @project.boards.size + @project.boards.each do |board| + assert !@source_project.boards.include?(board) + end + end + + test "#copy should change the new issues to use the copied issue categories" do + issue = Issue.find(4) + issue.update_attribute(:category_id, 3) + + assert @project.copy(@source_project) + + @project.issues.each do |issue| + assert issue.category + assert_equal "Stock management", issue.category.name # Same name + assert_not_equal IssueCategory.find(3), issue.category # Different record + end + end + + test "#copy should limit copy with :only option" do + assert @project.members.empty? + assert @project.issue_categories.empty? + assert @source_project.issues.any? + + assert @project.copy(@source_project, :only => ['members', 'issue_categories']) + + assert @project.members.any? + assert @project.issue_categories.any? + assert @project.issues.empty? + end + + test "#copy should copy subtasks" do + source = Project.generate!(:tracker_ids => [1]) + issue = Issue.generate_with_descendants!(:project => source) + project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1]) + + assert_difference 'Project.count' do + assert_difference 'Issue.count', 1+issue.descendants.count do + assert project.copy(source.reload) + end + end + copy = Issue.where(:parent_id => nil).order("id DESC").first + assert_equal project, copy.project + assert_equal issue.descendants.count, copy.descendants.count + child_copy = copy.children.detect {|c| c.subject == 'Child1'} + assert child_copy.descendants.any? + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/project_members_inheritance_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/project_members_inheritance_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,263 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 ProjectMembersInheritanceTest < ActiveSupport::TestCase + fixtures :roles, :users + + def setup + @parent = Project.generate! + @member = Member.create!(:principal => User.find(2), :project => @parent, :role_ids => [1, 2]) + assert_equal 2, @member.reload.roles.size + end + + def test_project_created_with_inherit_members_disabled_should_not_inherit_members + assert_no_difference 'Member.count' do + project = Project.generate_with_parent!(@parent, :inherit_members => false) + + assert_equal 0, project.memberships.count + end + end + + def test_project_created_with_inherit_members_should_inherit_members + assert_difference 'Member.count', 1 do + project = Project.generate_with_parent!(@parent, :inherit_members => true) + project.reload + + assert_equal 1, project.memberships.count + member = project.memberships.first + assert_equal @member.principal, member.principal + assert_equal @member.roles.sort, member.roles.sort + end + end + + def test_turning_on_inherit_members_should_inherit_members + Project.generate_with_parent!(@parent, :inherit_members => false) + + assert_difference 'Member.count', 1 do + project = Project.order('id desc').first + project.inherit_members = true + project.save! + project.reload + + assert_equal 1, project.memberships.count + member = project.memberships.first + assert_equal @member.principal, member.principal + assert_equal @member.roles.sort, member.roles.sort + end + end + + def test_turning_off_inherit_members_should_remove_inherited_members + Project.generate_with_parent!(@parent, :inherit_members => true) + + assert_difference 'Member.count', -1 do + project = Project.order('id desc').first + project.inherit_members = false + project.save! + project.reload + + assert_equal 0, project.memberships.count + end + end + + def test_moving_a_root_project_under_a_parent_should_inherit_members + Project.generate!(:inherit_members => true) + project = Project.order('id desc').first + + assert_difference 'Member.count', 1 do + project.set_parent!(@parent) + project.reload + + assert_equal 1, project.memberships.count + member = project.memberships.first + assert_equal @member.principal, member.principal + assert_equal @member.roles.sort, member.roles.sort + end + end + + def test_moving_a_subproject_as_root_should_loose_inherited_members + Project.generate_with_parent!(@parent, :inherit_members => true) + project = Project.order('id desc').first + + assert_difference 'Member.count', -1 do + project.set_parent!(nil) + project.reload + + assert_equal 0, project.memberships.count + end + end + + def test_moving_a_subproject_to_another_parent_should_change_inherited_members + other_parent = Project.generate! + other_member = Member.create!(:principal => User.find(4), :project => other_parent, :role_ids => [3]) + other_member.reload + + Project.generate_with_parent!(@parent, :inherit_members => true) + project = Project.order('id desc').first + project.set_parent!(other_parent.reload) + project.reload + + assert_equal 1, project.memberships.count + member = project.memberships.first + assert_equal other_member.principal, member.principal + assert_equal other_member.roles.sort, member.roles.sort + end + + def test_inheritance_should_propagate_to_subprojects + project = Project.generate_with_parent!(@parent, :inherit_members => false) + subproject = Project.generate_with_parent!(project, :inherit_members => true) + project.reload + + assert_difference 'Member.count', 2 do + project.inherit_members = true + project.save + project.reload + subproject.reload + + assert_equal 1, project.memberships.count + assert_equal 1, subproject.memberships.count + member = subproject.memberships.first + assert_equal @member.principal, member.principal + assert_equal @member.roles.sort, member.roles.sort + end + end + + def test_inheritance_removal_should_propagate_to_subprojects + project = Project.generate_with_parent!(@parent, :inherit_members => true) + subproject = Project.generate_with_parent!(project, :inherit_members => true) + project.reload + + assert_difference 'Member.count', -2 do + project.inherit_members = false + project.save + project.reload + subproject.reload + + assert_equal 0, project.memberships.count + assert_equal 0, subproject.memberships.count + end + end + + def test_adding_a_member_should_propagate + project = Project.generate_with_parent!(@parent, :inherit_members => true) + + assert_difference 'Member.count', 2 do + member = Member.create!(:principal => User.find(4), :project => @parent, :role_ids => [1, 3]) + member.reload + + inherited_member = project.memberships.order('id desc').first + assert_equal member.principal, inherited_member.principal + assert_equal member.roles.sort, inherited_member.roles.sort + end + end + + def test_adding_a_member_should_not_propagate_if_child_does_not_inherit + project = Project.generate_with_parent!(@parent, :inherit_members => false) + + assert_difference 'Member.count', 1 do + member = Member.create!(:principal => User.find(4), :project => @parent, :role_ids => [1, 3]) + + assert_nil project.reload.memberships.detect {|m| m.principal == member.principal} + end + end + + def test_removing_a_member_should_propagate + project = Project.generate_with_parent!(@parent, :inherit_members => true) + + assert_difference 'Member.count', -2 do + @member.reload.destroy + project.reload + + assert_equal 0, project.memberships.count + end + end + + def test_adding_a_group_member_should_propagate_with_its_users + project = Project.generate_with_parent!(@parent, :inherit_members => true) + group = Group.generate! + user = User.find(4) + group.users << user + + assert_difference 'Member.count', 4 do + assert_difference 'MemberRole.count', 8 do + member = Member.create!(:principal => group, :project => @parent, :role_ids => [1, 3]) + project.reload + member.reload + + inherited_group_member = project.memberships.detect {|m| m.principal == group} + assert_not_nil inherited_group_member + assert_equal member.roles.sort, inherited_group_member.roles.sort + + inherited_user_member = project.memberships.detect {|m| m.principal == user} + assert_not_nil inherited_user_member + assert_equal member.roles.sort, inherited_user_member.roles.sort + end + end + end + + def test_removing_a_group_member_should_propagate + project = Project.generate_with_parent!(@parent, :inherit_members => true) + group = Group.generate! + user = User.find(4) + group.users << user + member = Member.create!(:principal => group, :project => @parent, :role_ids => [1, 3]) + + assert_difference 'Member.count', -4 do + assert_difference 'MemberRole.count', -8 do + member.destroy + project.reload + + inherited_group_member = project.memberships.detect {|m| m.principal == group} + assert_nil inherited_group_member + + inherited_user_member = project.memberships.detect {|m| m.principal == user} + assert_nil inherited_user_member + end + end + end + + def test_adding_user_who_use_is_already_a_member_to_parent_project_should_merge_roles + project = Project.generate_with_parent!(@parent, :inherit_members => true) + user = User.find(4) + Member.create!(:principal => user, :project => project, :role_ids => [1, 2]) + + assert_difference 'Member.count', 1 do + Member.create!(:principal => User.find(4), :project => @parent.reload, :role_ids => [1, 3]) + + member = project.reload.memberships.detect {|m| m.principal == user} + assert_not_nil member + assert_equal [1, 2, 3], member.roles.uniq.sort.map(&:id) + end + end + + def test_turning_on_inheritance_with_user_who_is_already_a_member_should_merge_roles + project = Project.generate_with_parent!(@parent) + user = @member.user + Member.create!(:principal => user, :project => project, :role_ids => [1, 3]) + project.reload + + assert_no_difference 'Member.count' do + project.inherit_members = true + project.save! + + member = project.reload.memberships.detect {|m| m.principal == user} + assert_not_nil member + assert_equal [1, 2, 3], member.roles.uniq.sort.map(&:id) + end + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/project_nested_set_test.rb --- a/test/unit/project_nested_set_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/project_nested_set_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -50,12 +50,12 @@ def test_rebuild_should_build_valid_tree Project.update_all "lft = NULL, rgt = NULL" - Project.rebuild! + Project.rebuild_tree! assert_valid_nested_set end def test_rebuild_tree_should_build_valid_tree_even_with_valid_lft_rgt_values - Project.update_all "name = 'YY'", {:id => @a.id } + Project.where({:id => @a.id }).update_all("name = 'YY'") # lft and rgt values are still valid (Project.rebuild! would not update anything) # but projects are not ordered properly (YY is in the first place) diff -r d98d22a98252 -r fb9a13467253 test/unit/project_test.rb --- a/test/unit/project_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/project_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -30,7 +30,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :versions, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :groups_users, @@ -75,9 +74,30 @@ with_settings :default_projects_modules => ['issue_tracking', 'repository'] do assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names end + end - assert_equal Tracker.all.sort, Project.new.trackers.sort - assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort + def test_default_trackers_should_match_default_tracker_ids_setting + with_settings :default_projects_tracker_ids => ['1', '3'] do + assert_equal Tracker.find(1, 3).sort, Project.new.trackers.sort + end + end + + def test_default_trackers_should_be_all_trackers_with_blank_setting + with_settings :default_projects_tracker_ids => nil do + assert_equal Tracker.all.sort, Project.new.trackers.sort + end + end + + def test_default_trackers_should_be_empty_with_empty_setting + with_settings :default_projects_tracker_ids => [] do + assert_equal [], Project.new.trackers + end + end + + def test_default_trackers_should_not_replace_initialized_trackers + with_settings :default_projects_tracker_ids => ['1', '3'] do + assert_equal Tracker.find(1, 2).sort, Project.new(:tracker_ids => [1, 2]).trackers.sort + end end def test_update @@ -113,8 +133,7 @@ end def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier - Project.update_all(["identifier = ''"], "id = 1") - + Project.where(:id => 1).update_all(["identifier = ''"]) assert_equal false, Project.find(1).identifier_frozen? end @@ -155,7 +174,7 @@ # Assign an issue of a project to a version of a child project Issue.find(4).update_attribute :fixed_version_id, 4 - assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do + assert_no_difference "Project.where(:status => Project::STATUS_ARCHIVED).count" do assert_equal false, @ecookbook.archive end @ecookbook.reload @@ -183,7 +202,7 @@ # 2 active members assert_equal 2, @ecookbook.members.size # and 1 is locked - assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size + assert_equal 3, Member.where(:project_id => @ecookbook.id).count # some boards assert @ecookbook.boards.any? @@ -191,9 +210,9 @@ # make sure that the project non longer exists assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) } # make sure related data was removed - assert_nil Member.first(:conditions => {:project_id => @ecookbook.id}) - assert_nil Board.first(:conditions => {:project_id => @ecookbook.id}) - assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id}) + assert_nil Member.where(:project_id => @ecookbook.id).first + assert_nil Board.where(:project_id => @ecookbook.id).first + assert_nil Issue.where(:project_id => @ecookbook.id).first end def test_destroy_should_destroy_subtasks @@ -205,7 +224,7 @@ assert_nothing_raised do Project.find(1).destroy end - assert Issue.find_all_by_id(issues.map(&:id)).empty? + assert_equal 0, Issue.where(:id => issues.map(&:id)).count end def test_destroying_root_projects_should_clear_data @@ -226,7 +245,7 @@ assert_equal 0, Board.count assert_equal 0, Message.count assert_equal 0, News.count - assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL") + assert_equal 0, Query.where("project_id IS NOT NULL").count assert_equal 0, Repository.count assert_equal 0, Changeset.count assert_equal 0, Change.count @@ -238,9 +257,18 @@ assert_equal 0, WikiPage.count assert_equal 0, WikiContent.count assert_equal 0, WikiContent::Version.count - assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size - assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size - assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']}) + assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").count + assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").count + assert_equal 0, CustomValue.where(:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']).count + end + + def test_destroy_should_delete_time_entries_custom_values + project = Project.generate! + time_entry = TimeEntry.generate!(:project => project, :custom_field_values => {10 => '1'}) + + assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do + assert project.destroy + end end def test_move_an_orphan_project_to_a_root_project @@ -435,56 +463,67 @@ assert_equal [1,2], parent.rolled_up_trackers.collect(&:id) end - context "#rolled_up_versions" do - setup do - @project = Project.generate! - @parent_version_1 = Version.generate!(:project => @project) - @parent_version_2 = Version.generate!(:project => @project) - end + test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do + parent = Project.generate! + parent.trackers = Tracker.find([1, 2]) + child = Project.generate_with_parent!(parent) + child.trackers = Tracker.find([2, 3]) - should "include the versions for the current project" do - assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions - end + assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort - should "include versions for a subproject" do - @subproject = Project.generate! - @subproject.set_parent!(@project) - @subproject_version = Version.generate!(:project => @subproject) + assert child.disable_module!(:issue_tracking) + parent.reload + assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort + end - assert_same_elements [ - @parent_version_1, - @parent_version_2, - @subproject_version - ], @project.rolled_up_versions - end + test "#rolled_up_versions should include the versions for the current project" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions + end - should "include versions for a sub-subproject" do - @subproject = Project.generate! - @subproject.set_parent!(@project) - @sub_subproject = Project.generate! - @sub_subproject.set_parent!(@subproject) - @sub_subproject_version = Version.generate!(:project => @sub_subproject) + test "#rolled_up_versions should include versions for a subproject" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + subproject = Project.generate_with_parent!(project) + subproject_version = Version.generate!(:project => subproject) - @project.reload + assert_same_elements [ + parent_version_1, + parent_version_2, + subproject_version + ], project.rolled_up_versions + end - assert_same_elements [ - @parent_version_1, - @parent_version_2, - @sub_subproject_version - ], @project.rolled_up_versions - end + test "#rolled_up_versions should include versions for a sub-subproject" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + subproject = Project.generate_with_parent!(project) + sub_subproject = Project.generate_with_parent!(subproject) + sub_subproject_version = Version.generate!(:project => sub_subproject) + project.reload - should "only check active projects" do - @subproject = Project.generate! - @subproject.set_parent!(@project) - @subproject_version = Version.generate!(:project => @subproject) - assert @subproject.archive + assert_same_elements [ + parent_version_1, + parent_version_2, + sub_subproject_version + ], project.rolled_up_versions + end - @project.reload + test "#rolled_up_versions should only check active projects" do + project = Project.generate! + parent_version_1 = Version.generate!(:project => project) + parent_version_2 = Version.generate!(:project => project) + subproject = Project.generate_with_parent!(project) + subproject_version = Version.generate!(:project => subproject) + assert subproject.archive + project.reload - assert !@subproject.active? - assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions - end + assert !subproject.active? + assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions end def test_shared_versions_none_sharing @@ -545,7 +584,7 @@ assert_equal [1,2,3], parent.version_ids.sort assert_equal [4], child.version_ids assert_equal [6], private_child.version_ids - assert_equal [7], Version.find_all_by_sharing('system').collect(&:id) + assert_equal [7], Version.where(:sharing => 'system').collect(&:id) assert_equal 6, parent.shared_versions.size parent.shared_versions.each do |version| @@ -611,52 +650,49 @@ end end - context "enabled_modules" do - setup do - @project = Project.find(1) + test "enabled_modules should define module by names and preserve ids" do + @project = Project.find(1) + # Remove one module + modules = @project.enabled_modules.slice(0..-2) + assert modules.any? + assert_difference 'EnabledModule.count', -1 do + @project.enabled_module_names = modules.collect(&:name) end + @project.reload + # Ids should be preserved + assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort + end - should "define module by names and preserve ids" do - # Remove one module - modules = @project.enabled_modules.slice(0..-2) - assert modules.any? - assert_difference 'EnabledModule.count', -1 do - @project.enabled_module_names = modules.collect(&:name) - end - @project.reload - # Ids should be preserved - assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort - end + test "enabled_modules should enable a module" do + @project = Project.find(1) + @project.enabled_module_names = [] + @project.reload + assert_equal [], @project.enabled_module_names + #with string + @project.enable_module!("issue_tracking") + assert_equal ["issue_tracking"], @project.enabled_module_names + #with symbol + @project.enable_module!(:gantt) + assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names + #don't add a module twice + @project.enable_module!("issue_tracking") + assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names + end - should "enable a module" do - @project.enabled_module_names = [] - @project.reload - assert_equal [], @project.enabled_module_names - #with string - @project.enable_module!("issue_tracking") - assert_equal ["issue_tracking"], @project.enabled_module_names - #with symbol - @project.enable_module!(:gantt) - assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names - #don't add a module twice - @project.enable_module!("issue_tracking") - assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names - end - - should "disable a module" do - #with string - assert @project.enabled_module_names.include?("issue_tracking") - @project.disable_module!("issue_tracking") - assert ! @project.reload.enabled_module_names.include?("issue_tracking") - #with symbol - assert @project.enabled_module_names.include?("gantt") - @project.disable_module!(:gantt) - assert ! @project.reload.enabled_module_names.include?("gantt") - #with EnabledModule object - first_module = @project.enabled_modules.first - @project.disable_module!(first_module) - assert ! @project.reload.enabled_module_names.include?(first_module.name) - end + test "enabled_modules should disable a module" do + @project = Project.find(1) + #with string + assert @project.enabled_module_names.include?("issue_tracking") + @project.disable_module!("issue_tracking") + assert ! @project.reload.enabled_module_names.include?("issue_tracking") + #with symbol + assert @project.enabled_module_names.include?("gantt") + @project.disable_module!(:gantt) + assert ! @project.reload.enabled_module_names.include?("gantt") + #with EnabledModule object + first_module = @project.enabled_modules.first + @project.disable_module!(first_module) + assert ! @project.reload.enabled_module_names.include?(first_module.name) end def test_enabled_module_names_should_not_recreate_enabled_modules @@ -693,7 +729,8 @@ def test_activities_should_use_the_system_activities project = Project.find(1) - assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} ) + assert_equal project.activities, TimeEntryActivity.where(:active => true).all + assert_kind_of ActiveRecord::Relation, project.activities end @@ -703,11 +740,12 @@ assert overridden_activity.save! assert project.activities.include?(overridden_activity), "Project specific Activity not found" + assert_kind_of ActiveRecord::Relation, project.activities end def test_activities_should_not_include_the_inactive_project_specific_activities project = Project.find(1) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false}) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false}) assert overridden_activity.save! assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found" @@ -722,7 +760,7 @@ end def test_activities_should_handle_nils - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)}) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first}) TimeEntryActivity.delete_all # No activities @@ -737,7 +775,7 @@ def test_activities_should_override_system_activities_with_project_activities project = Project.find(1) - parent_activity = TimeEntryActivity.find(:first) + parent_activity = TimeEntryActivity.first overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity}) assert overridden_activity.save! @@ -747,7 +785,7 @@ def test_activities_should_include_inactive_activities_if_specified project = Project.find(1) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false}) + overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false}) assert overridden_activity.save! assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found" @@ -775,438 +813,135 @@ assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'} end - context "Project#copy" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - Project.destroy_all :identifier => "copy-test" - @source_project = Project.find(2) - @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test') - @project.trackers = @source_project.trackers - @project.enabled_module_names = @source_project.enabled_modules.collect(&:name) - end - - should "copy issues" do - @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'), - :subject => "copy issue status", - :tracker_id => 1, - :assigned_to_id => 2, - :project_id => @source_project.id) - assert @project.valid? - assert @project.issues.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.issues.size, @project.issues.size - @project.issues.each do |issue| - assert issue.valid? - assert ! issue.assigned_to.blank? - assert_equal @project, issue.project - end - - copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"}) - assert copied_issue - assert copied_issue.status - assert_equal "Closed", copied_issue.status.name - end - - should "copy issues assigned to a locked version" do - User.current = User.find(1) - assigned_version = Version.generate!(:name => "Assigned Issues") - @source_project.versions << assigned_version - Issue.generate!(:project => @source_project, - :fixed_version_id => assigned_version.id, - :subject => "copy issues assigned to a locked version") - assigned_version.update_attribute :status, 'locked' - - assert @project.copy(@source_project) - @project.reload - copied_issue = @project.issues.first(:conditions => {:subject => "copy issues assigned to a locked version"}) - - assert copied_issue - assert copied_issue.fixed_version - assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name - assert_equal 'locked', copied_issue.fixed_version.status - end - - should "change the new issues to use the copied version" do - User.current = User.find(1) - assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open') - @source_project.versions << assigned_version - assert_equal 3, @source_project.versions.size - Issue.generate!(:project => @source_project, - :fixed_version_id => assigned_version.id, - :subject => "change the new issues to use the copied version") - - assert @project.copy(@source_project) - @project.reload - copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"}) - - assert copied_issue - assert copied_issue.fixed_version - assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name - assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record - end - - should "keep target shared versions from other project" do - assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system') - issue = Issue.generate!(:project => @source_project, - :fixed_version => assigned_version, - :subject => "keep target shared versions") - - assert @project.copy(@source_project) - @project.reload - copied_issue = @project.issues.first(:conditions => {:subject => "keep target shared versions"}) - - assert copied_issue - assert_equal assigned_version, copied_issue.fixed_version - end - - should "copy issue relations" do - Setting.cross_project_issue_relations = '1' - - second_issue = Issue.generate!(:status_id => 5, - :subject => "copy issue relation", - :tracker_id => 1, - :assigned_to_id => 2, - :project_id => @source_project.id) - source_relation = IssueRelation.create!(:issue_from => Issue.find(4), - :issue_to => second_issue, - :relation_type => "relates") - source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1), - :issue_to => second_issue, - :relation_type => "duplicates") - - assert @project.copy(@source_project) - assert_equal @source_project.issues.count, @project.issues.count - copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4 - copied_second_issue = @project.issues.find_by_subject("copy issue relation") - - # First issue with a relation on project - assert_equal 1, copied_issue.relations.size, "Relation not copied" - copied_relation = copied_issue.relations.first - assert_equal "relates", copied_relation.relation_type - assert_equal copied_second_issue.id, copied_relation.issue_to_id - assert_not_equal source_relation.id, copied_relation.id - - # Second issue with a cross project relation - assert_equal 2, copied_second_issue.relations.size, "Relation not copied" - copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first - assert_equal "duplicates", copied_relation.relation_type - assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept" - assert_not_equal source_relation_cross_project.id, copied_relation.id - end - - should "copy issue attachments" do - issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id) - Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1) - @source_project.issues << issue - assert @project.copy(@source_project) - - copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"}) - assert_not_nil copied_issue - assert_equal 1, copied_issue.attachments.count, "Attachment not copied" - assert_equal "testfile.txt", copied_issue.attachments.first.filename - end - - should "copy memberships" do - assert @project.valid? - assert @project.members.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.memberships.size, @project.memberships.size - @project.memberships.each do |membership| - assert membership - assert_equal @project, membership.project - end - end - - should "copy memberships with groups and additional roles" do - group = Group.create!(:lastname => "Copy group") - user = User.find(7) - group.users << user - # group role - Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2]) - member = Member.find_by_user_id_and_project_id(user.id, @source_project.id) - # additional role - member.role_ids = [1] - - assert @project.copy(@source_project) - member = Member.find_by_user_id_and_project_id(user.id, @project.id) - assert_not_nil member - assert_equal [1, 2], member.role_ids.sort - end - - should "copy project specific queries" do - assert @project.valid? - assert @project.queries.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.queries.size, @project.queries.size - @project.queries.each do |query| - assert query - assert_equal @project, query.project - end - assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort - end - - should "copy versions" do - @source_project.versions << Version.generate! - @source_project.versions << Version.generate! - - assert @project.versions.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.versions.size, @project.versions.size - @project.versions.each do |version| - assert version - assert_equal @project, version.project - end - end - - should "copy wiki" do - assert_difference 'Wiki.count' do - assert @project.copy(@source_project) - end - - assert @project.wiki - assert_not_equal @source_project.wiki, @project.wiki - assert_equal "Start page", @project.wiki.start_page - end - - should "copy wiki pages and content with hierarchy" do - assert_difference 'WikiPage.count', @source_project.wiki.pages.size do - assert @project.copy(@source_project) - end - - assert @project.wiki - assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size - - @project.wiki.pages.each do |wiki_page| - assert wiki_page.content - assert !@source_project.wiki.pages.include?(wiki_page) - end - - parent = @project.wiki.find_page('Parent_page') - child1 = @project.wiki.find_page('Child_page_1') - child2 = @project.wiki.find_page('Child_page_2') - assert_equal parent, child1.parent - assert_equal parent, child2.parent - end - - should "copy issue categories" do - assert @project.copy(@source_project) - - assert_equal 2, @project.issue_categories.size - @project.issue_categories.each do |issue_category| - assert !@source_project.issue_categories.include?(issue_category) - end - end - - should "copy boards" do - assert @project.copy(@source_project) - - assert_equal 1, @project.boards.size - @project.boards.each do |board| - assert !@source_project.boards.include?(board) - end - end - - should "change the new issues to use the copied issue categories" do - issue = Issue.find(4) - issue.update_attribute(:category_id, 3) - - assert @project.copy(@source_project) - - @project.issues.each do |issue| - assert issue.category - assert_equal "Stock management", issue.category.name # Same name - assert_not_equal IssueCategory.find(3), issue.category # Different record - end - end - - should "limit copy with :only option" do - assert @project.members.empty? - assert @project.issue_categories.empty? - assert @source_project.issues.any? - - assert @project.copy(@source_project, :only => ['members', 'issue_categories']) - - assert @project.members.any? - assert @project.issue_categories.any? - assert @project.issues.empty? - end + test "#start_date should be nil if there are no issues on the project" do + project = Project.generate! + assert_nil project.start_date end - def test_copy_should_copy_subtasks - source = Project.generate!(:tracker_ids => [1]) - issue = Issue.generate_with_descendants!(:project => source) - project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1]) + test "#start_date should be nil when issues have no start date" do + project = Project.generate! + project.trackers << Tracker.generate! + early = 7.days.ago.to_date + Issue.generate!(:project => project, :start_date => nil) - assert_difference 'Project.count' do - assert_difference 'Issue.count', 1+issue.descendants.count do - assert project.copy(source.reload) - end - end - copy = Issue.where(:parent_id => nil).order("id DESC").first - assert_equal project, copy.project - assert_equal issue.descendants.count, copy.descendants.count - child_copy = copy.children.detect {|c| c.subject == 'Child1'} - assert child_copy.descendants.any? + assert_nil project.start_date end - context "#start_date" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.generate!(:identifier => 'test0') - @project.trackers << Tracker.generate! - end + test "#start_date should be the earliest start date of it's issues" do + project = Project.generate! + project.trackers << Tracker.generate! + early = 7.days.ago.to_date + Issue.generate!(:project => project, :start_date => Date.today) + Issue.generate!(:project => project, :start_date => early) - should "be nil if there are no issues on the project" do - assert_nil @project.start_date - end - - should "be tested when issues have no start date" - - should "be the earliest start date of it's issues" do - early = 7.days.ago.to_date - Issue.generate!(:project => @project, :start_date => Date.today) - Issue.generate!(:project => @project, :start_date => early) - - assert_equal early, @project.start_date - end - + assert_equal early, project.start_date end - context "#due_date" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.generate!(:identifier => 'test0') - @project.trackers << Tracker.generate! - end - - should "be nil if there are no issues on the project" do - assert_nil @project.due_date - end - - should "be tested when issues have no due date" - - should "be the latest due date of it's issues" do - future = 7.days.from_now.to_date - Issue.generate!(:project => @project, :due_date => future) - Issue.generate!(:project => @project, :due_date => Date.today) - - assert_equal future, @project.due_date - end - - should "be the latest due date of it's versions" do - future = 7.days.from_now.to_date - @project.versions << Version.generate!(:effective_date => future) - @project.versions << Version.generate!(:effective_date => Date.today) - - - assert_equal future, @project.due_date - - end - - should "pick the latest date from it's issues and versions" do - future = 7.days.from_now.to_date - far_future = 14.days.from_now.to_date - Issue.generate!(:project => @project, :due_date => far_future) - @project.versions << Version.generate!(:effective_date => future) - - assert_equal far_future, @project.due_date - end - + test "#due_date should be nil if there are no issues on the project" do + project = Project.generate! + assert_nil project.due_date end - context "Project#completed_percent" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.generate!(:identifier => 'test0') - @project.trackers << Tracker.generate! - end + test "#due_date should be nil if there are no issues with due dates" do + project = Project.generate! + project.trackers << Tracker.generate! + Issue.generate!(:project => project, :due_date => nil) - context "no versions" do - should "be 100" do - assert_equal 100, @project.completed_percent - end - end - - context "with versions" do - should "return 0 if the versions have no issues" do - Version.generate!(:project => @project) - Version.generate!(:project => @project) - - assert_equal 0, @project.completed_percent - end - - should "return 100 if the version has only closed issues" do - v1 = Version.generate!(:project => @project) - Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1) - v2 = Version.generate!(:project => @project) - Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2) - - assert_equal 100, @project.completed_percent - end - - should "return the averaged completed percent of the versions (not weighted)" do - v1 = Version.generate!(:project => @project) - Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1) - v2 = Version.generate!(:project => @project) - Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2) - - assert_equal 50, @project.completed_percent - end - - end + assert_nil project.due_date end - context "#notified_users" do - setup do - @project = Project.generate! - @role = Role.generate! + test "#due_date should be the latest due date of it's issues" do + project = Project.generate! + project.trackers << Tracker.generate! + future = 7.days.from_now.to_date + Issue.generate!(:project => project, :due_date => future) + Issue.generate!(:project => project, :due_date => Date.today) - @user_with_membership_notification = User.generate!(:mail_notification => 'selected') - Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true) - - @all_events_user = User.generate!(:mail_notification => 'all') - Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user) - - @no_events_user = User.generate!(:mail_notification => 'none') - Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user) - - @only_my_events_user = User.generate!(:mail_notification => 'only_my_events') - Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user) - - @only_assigned_user = User.generate!(:mail_notification => 'only_assigned') - Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user) - - @only_owned_user = User.generate!(:mail_notification => 'only_owner') - Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user) - end - - should "include members with a mail notification" do - assert @project.notified_users.include?(@user_with_membership_notification) - end - - should "include users with the 'all' notification option" do - assert @project.notified_users.include?(@all_events_user) - end - - should "not include users with the 'none' notification option" do - assert !@project.notified_users.include?(@no_events_user) - end - - should "not include users with the 'only_my_events' notification option" do - assert !@project.notified_users.include?(@only_my_events_user) - end - - should "not include users with the 'only_assigned' notification option" do - assert !@project.notified_users.include?(@only_assigned_user) - end - - should "not include users with the 'only_owner' notification option" do - assert !@project.notified_users.include?(@only_owned_user) - end + assert_equal future, project.due_date end + test "#due_date should be the latest due date of it's versions" do + project = Project.generate! + future = 7.days.from_now.to_date + project.versions << Version.generate!(:effective_date => future) + project.versions << Version.generate!(:effective_date => Date.today) + + assert_equal future, project.due_date + end + + test "#due_date should pick the latest date from it's issues and versions" do + project = Project.generate! + project.trackers << Tracker.generate! + future = 7.days.from_now.to_date + far_future = 14.days.from_now.to_date + Issue.generate!(:project => project, :due_date => far_future) + project.versions << Version.generate!(:effective_date => future) + + assert_equal far_future, project.due_date + end + + test "#completed_percent with no versions should be 100" do + project = Project.generate! + assert_equal 100, project.completed_percent + end + + test "#completed_percent with versions should return 0 if the versions have no issues" do + project = Project.generate! + Version.generate!(:project => project) + Version.generate!(:project => project) + + assert_equal 0, project.completed_percent + end + + test "#completed_percent with versions should return 100 if the version has only closed issues" do + project = Project.generate! + project.trackers << Tracker.generate! + v1 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1) + v2 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2) + + assert_equal 100, project.completed_percent + end + + test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do + project = Project.generate! + project.trackers << Tracker.generate! + v1 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1) + v2 = Version.generate!(:project => project) + Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2) + + assert_equal 50, project.completed_percent + end + + test "#notified_users" do + project = Project.generate! + role = Role.generate! + + user_with_membership_notification = User.generate!(:mail_notification => 'selected') + Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true) + + all_events_user = User.generate!(:mail_notification => 'all') + Member.create!(:project => project, :roles => [role], :principal => all_events_user) + + no_events_user = User.generate!(:mail_notification => 'none') + Member.create!(:project => project, :roles => [role], :principal => no_events_user) + + only_my_events_user = User.generate!(:mail_notification => 'only_my_events') + Member.create!(:project => project, :roles => [role], :principal => only_my_events_user) + + only_assigned_user = User.generate!(:mail_notification => 'only_assigned') + Member.create!(:project => project, :roles => [role], :principal => only_assigned_user) + + only_owned_user = User.generate!(:mail_notification => 'only_owner') + Member.create!(:project => project, :roles => [role], :principal => only_owned_user) + + assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification" + assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option" + assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option" + assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option" + assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option" + assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option" + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/query_test.rb --- a/test/unit/query_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/query_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -28,21 +28,71 @@ :projects_trackers, :custom_fields_trackers + def test_query_with_roles_visibility_should_validate_roles + set_language_if_valid 'en' + query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES) + assert !query.save + assert_include "Roles can't be blank", query.errors.full_messages + query.role_ids = [1, 2] + assert query.save + end + + def test_changing_roles_visibility_should_clear_roles + query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2]) + assert_equal 2, query.roles.count + + query.visibility = IssueQuery::VISIBILITY_PUBLIC + query.save! + assert_equal 0, query.roles.count + end + + def test_available_filters_should_be_ordered + set_language_if_valid 'en' + query = IssueQuery.new + assert_equal 0, query.available_filters.keys.index('status_id') + expected_order = [ + "Status", + "Project", + "Tracker", + "Priority" + ] + assert_equal expected_order, + (query.available_filters.values.map{|v| v[:name]} & expected_order) + end + + def test_available_filters_with_custom_fields_should_be_ordered + set_language_if_valid 'en' + UserCustomField.create!( + :name => 'order test', :field_format => 'string', + :is_for_all => true, :is_filter => true + ) + query = IssueQuery.new + expected_order = [ + "Searchable field", + "Database", + "Project's Development status", + "Author's order test", + "Assignee's order test" + ] + assert_equal expected_order, + (query.available_filters.values.map{|v| v[:name]} & expected_order) + end + def test_custom_fields_for_all_projects_should_be_available_in_global_queries - query = Query.new(:project => nil, :name => '_') + query = IssueQuery.new(:project => nil, :name => '_') assert query.available_filters.has_key?('cf_1') assert !query.available_filters.has_key?('cf_3') end def test_system_shared_versions_should_be_available_in_global_queries Version.find(2).update_attribute :sharing, 'system' - query = Query.new(:project => nil, :name => '_') + query = IssueQuery.new(:project => nil, :name => '_') assert query.available_filters.has_key?('fixed_version_id') assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'} end def test_project_filter_in_global_queries - query = Query.new(:project => nil, :name => '_') + query = IssueQuery.new(:project => nil, :name => '_') project_filter = query.available_filters["project_id"] assert_not_nil project_filter project_ids = project_filter[:values].map{|p| p[1]} @@ -63,9 +113,9 @@ end def assert_query_statement_includes(query, condition) - assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}" + assert_include condition, query.statement end - + def assert_query_result(expected, query) assert_nothing_raised do assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort @@ -75,14 +125,14 @@ def test_query_should_allow_shared_versions_for_a_project_query subproject_version = Version.find(4) - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s]) assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')") end def test_query_with_multiple_custom_fields - query = Query.find(1) + query = IssueQuery.find(1) assert query.valid? assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')") issues = find_issues_with_query(query) @@ -91,7 +141,7 @@ end def test_operator_none - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('fixed_version_id', '!*', ['']) query.add_filter('cf_1', '!*', ['']) assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL") @@ -100,7 +150,7 @@ end def test_operator_none_for_integer - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('estimated_hours', '!*', ['']) issues = find_issues_with_query(query) assert !issues.empty? @@ -108,7 +158,7 @@ end def test_operator_none_for_date - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('start_date', '!*', ['']) issues = find_issues_with_query(query) assert !issues.empty? @@ -116,7 +166,7 @@ end def test_operator_none_for_string_custom_field - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('cf_2', '!*', ['']) assert query.has_filter?('cf_2') issues = find_issues_with_query(query) @@ -125,7 +175,7 @@ end def test_operator_all - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('fixed_version_id', '*', ['']) query.add_filter('cf_1', '*', ['']) assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL") @@ -134,7 +184,7 @@ end def test_operator_all_for_date - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('start_date', '*', ['']) issues = find_issues_with_query(query) assert !issues.empty? @@ -142,7 +192,7 @@ end def test_operator_all_for_string_custom_field - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('cf_2', '*', ['']) assert query.has_filter?('cf_2') issues = find_issues_with_query(query) @@ -151,7 +201,7 @@ end def test_numeric_filter_should_not_accept_non_numeric_values - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('estimated_hours', '=', ['a']) assert query.has_filter?('estimated_hours') @@ -159,9 +209,8 @@ end def test_operator_is_on_float - Issue.update_all("estimated_hours = 171.2", "id=2") - - query = Query.new(:name => '_') + Issue.where(:id => 2).update_all("estimated_hours = 171.2") + query = IssueQuery.new(:name => '_') query.add_filter('estimated_hours', '=', ['171.20']) issues = find_issues_with_query(query) assert_equal 1, issues.size @@ -169,12 +218,12 @@ end def test_operator_is_on_integer_custom_field - f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true) + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '=', ['12']) issues = find_issues_with_query(query) assert_equal 1, issues.size @@ -182,12 +231,12 @@ end def test_operator_is_on_integer_custom_field_should_accept_negative_value - f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true) + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '=', ['-12']) assert query.valid? issues = find_issues_with_query(query) @@ -196,12 +245,12 @@ end def test_operator_is_on_float_custom_field - f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true) + f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3') CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '=', ['12.7']) issues = find_issues_with_query(query) assert_equal 1, issues.size @@ -209,12 +258,12 @@ end def test_operator_is_on_float_custom_field_should_accept_negative_value - f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true) + f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3') CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '=', ['-12.7']) assert query.valid? issues = find_issues_with_query(query) @@ -224,17 +273,17 @@ def test_operator_is_on_multi_list_custom_field f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true, - :possible_values => ['value1', 'value2', 'value3'], :multiple => true) + :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1') CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '=', ['value1']) issues = find_issues_with_query(query) assert_equal [1, 3], issues.map(&:id).sort - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '=', ['value2']) issues = find_issues_with_query(query) assert_equal [1], issues.map(&:id).sort @@ -242,18 +291,18 @@ def test_operator_is_not_on_multi_list_custom_field f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true, - :possible_values => ['value1', 'value2', 'value3'], :multiple => true) + :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1') CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '!', ['value1']) issues = find_issues_with_query(query) assert !issues.map(&:id).include?(1) assert !issues.map(&:id).include?(3) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter("cf_#{f.id}", '!', ['value2']) issues = find_issues_with_query(query) assert !issues.map(&:id).include?(1) @@ -264,7 +313,7 @@ # is_private filter only available for those who can set issues private User.current = User.find(2) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') assert query.available_filters.key?('is_private') query.add_filter("is_private", '=', ['1']) @@ -279,7 +328,7 @@ # is_private filter only available for those who can set issues private User.current = User.find(2) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') assert query.available_filters.key?('is_private') query.add_filter("is_private", '!', ['1']) @@ -291,26 +340,26 @@ end def test_operator_greater_than - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('done_ratio', '>=', ['40']) assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0") find_issues_with_query(query) end def test_operator_greater_than_a_float - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('estimated_hours', '>=', ['40.5']) assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5") find_issues_with_query(query) end def test_operator_greater_than_on_int_custom_field - f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12') CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter("cf_#{f.id}", '>=', ['8']) issues = find_issues_with_query(query) assert_equal 1, issues.size @@ -318,7 +367,7 @@ end def test_operator_lesser_than - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('done_ratio', '<=', ['30']) assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0") find_issues_with_query(query) @@ -326,14 +375,28 @@ def test_operator_lesser_than_on_custom_field f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter("cf_#{f.id}", '<=', ['30']) - assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0") + assert_match /CAST.+ <= 30\.0/, query.statement find_issues_with_query(query) end + def test_operator_lesser_than_on_date_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = IssueQuery.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '<=', ['2013-05-01']) + issue_ids = find_issues_with_query(query).map(&:id) + assert_include 1, issue_ids + assert_not_include 2, issue_ids + assert_not_include 3, issue_ids + end + def test_operator_between - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('done_ratio', '><', ['30', '40']) assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement find_issues_with_query(query) @@ -341,14 +404,14 @@ def test_operator_between_on_custom_field f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter("cf_#{f.id}", '><', ['30', '40']) - assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement + assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement find_issues_with_query(query) end def test_date_filter_should_not_accept_non_date_values - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('created_on', '=', ['a']) assert query.has_filter?('created_on') @@ -356,7 +419,7 @@ end def test_date_filter_should_not_accept_invalid_date_values - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('created_on', '=', ['2011-01-34']) assert query.has_filter?('created_on') @@ -364,7 +427,7 @@ end def test_relative_date_filter_should_not_accept_non_integer_values - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('created_on', '>t-', ['a']) assert query.has_filter?('created_on') @@ -372,36 +435,50 @@ end def test_operator_date_equals - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('due_date', '=', ['2011-07-10']) assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement find_issues_with_query(query) end def test_operator_date_lesser_than - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('due_date', '<=', ['2011-07-10']) assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement find_issues_with_query(query) end + def test_operator_date_lesser_than_with_timestamp + query = IssueQuery.new(:name => '_') + query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52']) + assert_match /issues\.updated_on <= '2011-07-10 19:13:52/, query.statement + find_issues_with_query(query) + end + def test_operator_date_greater_than - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('due_date', '>=', ['2011-07-10']) assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement find_issues_with_query(query) end + def test_operator_date_greater_than_with_timestamp + query = IssueQuery.new(:name => '_') + query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52']) + assert_match /issues\.updated_on > '2011-07-10 19:13:51(\.0+)?'/, query.statement + find_issues_with_query(query) + end + def test_operator_date_between - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10']) - assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement + assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?'/, query.statement find_issues_with_query(query) end def test_operator_in_more_than Issue.find(7).update_attribute(:due_date, (Date.today + 15)) - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', '>t+', ['15']) issues = find_issues_with_query(query) assert !issues.empty? @@ -409,7 +486,7 @@ end def test_operator_in_less_than - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', ' Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', '> Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', '>t-', ['3']) issues = find_issues_with_query(query) assert !issues.empty? @@ -435,7 +512,7 @@ def test_operator_in_the_past_days Issue.find(7).update_attribute(:due_date, (Date.today - 3)) - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', '> Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', ' Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', 'w', ['']) assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}" I18n.locale = :en @@ -517,13 +594,13 @@ Date.stubs(:today).returns(Date.parse('2011-04-29')) - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', 'w', ['']) assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}" end def test_operator_does_not_contains - query = Query.new(:project => Project.find(1), :name => '_') + query = IssueQuery.new(:project => Project.find(1), :name => '_') query.add_filter('subject', '!~', ['uNable']) assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'") find_issues_with_query(query) @@ -538,9 +615,9 @@ i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11)) group.users << user - query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}}) + query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}}) result = query.issues - assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id) + assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id) assert result.include?(i1) assert result.include?(i2) @@ -553,7 +630,7 @@ issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1) issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'}) - query = Query.new(:name => '_', :project => Project.find(1)) + query = IssueQuery.new(:name => '_', :project => Project.find(1)) filter = query.available_filters["cf_#{cf.id}"] assert_not_nil filter assert_include 'me', filter[:values].map{|v| v[1]} @@ -566,7 +643,7 @@ def test_filter_my_projects User.current = User.find(2) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') filter = query.available_filters['project_id'] assert_not_nil filter assert_include 'mine', filter[:values].map{|v| v[1]} @@ -578,7 +655,7 @@ def test_filter_watched_issues User.current = User.find(1) - query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}}) + query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}}) result = find_issues_with_query(query) assert_not_nil result assert !result.empty? @@ -588,7 +665,7 @@ def test_filter_unwatched_issues User.current = User.find(1) - query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}}) + query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}}) result = find_issues_with_query(query) assert_not_nil result assert !result.empty? @@ -596,12 +673,40 @@ User.current = nil end + def test_filter_on_custom_field_should_ignore_projects_with_field_disabled + field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_filter => true) + Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'}) + Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'}) + + query = IssueQuery.new(:name => '_', :project => Project.find(1)) + query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}} + assert_equal 2, find_issues_with_query(query).size + + field.project_ids = [1, 3] # Disable the field for project 4 + field.save! + assert_equal 1, find_issues_with_query(query).size + end + + def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled + field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true) + Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'}) + Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'}) + + query = IssueQuery.new(:name => '_', :project => Project.find(1)) + query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}} + assert_equal 2, find_issues_with_query(query).size + + field.tracker_ids = [1] # Disable the field for tracker 2 + field.save! + assert_equal 1, find_issues_with_query(query).size + end + def test_filter_on_project_custom_field field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo') CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') filter_name = "project.cf_#{field.id}" assert_include filter_name, query.available_filters.keys query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} @@ -612,7 +717,7 @@ field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') filter_name = "author.cf_#{field.id}" assert_include filter_name, query.available_filters.keys query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} @@ -623,7 +728,7 @@ field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') filter_name = "assigned_to.cf_#{field.id}" assert_include filter_name, query.available_filters.keys query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} @@ -634,7 +739,7 @@ field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo') - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') filter_name = "fixed_version.cf_#{field.id}" assert_include filter_name, query.available_filters.keys query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} @@ -646,11 +751,11 @@ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '=', :values => ['1']}} assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '=', :values => ['2']}} assert_equal [1], find_issues_with_query(query).map(&:id).sort end @@ -663,15 +768,15 @@ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first) end - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '=p', :values => ['2']}} assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '=p', :values => ['3']}} assert_equal [1], find_issues_with_query(query).map(&:id).sort - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '=p', :values => ['4']}} assert_equal [], find_issues_with_query(query).map(&:id).sort end @@ -684,7 +789,7 @@ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first) end - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '=!p', :values => ['1']}} assert_equal [1], find_issues_with_query(query).map(&:id).sort end @@ -697,7 +802,7 @@ IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3)) end - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '!p', :values => ['2']}} ids = find_issues_with_query(query).map(&:id).sort assert_include 2, ids @@ -710,7 +815,7 @@ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '!*', :values => ['']}} ids = find_issues_with_query(query).map(&:id) assert_equal [], ids & [1, 2, 3] @@ -722,13 +827,28 @@ IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.filters = {"relates" => {:operator => '*', :values => ['']}} assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort end + def test_filter_on_relations_should_not_ignore_other_filter + issue = Issue.generate! + issue1 = Issue.generate!(:status_id => 1) + issue2 = Issue.generate!(:status_id => 2) + IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1) + IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2) + + query = IssueQuery.new(:name => '_') + query.filters = { + "status_id" => {:operator => '=', :values => ['1']}, + "relates" => {:operator => '=', :values => [issue.id.to_s]} + } + assert_equal [issue1], find_issues_with_query(query) + end + def test_statement_should_be_nil_with_no_filters - q = Query.new(:name => '_') + q = IssueQuery.new(:name => '_') q.filters = {} assert q.valid? @@ -736,44 +856,62 @@ end def test_default_columns - q = Query.new + q = IssueQuery.new assert q.columns.any? assert q.inline_columns.any? assert q.block_columns.empty? end def test_set_column_names - q = Query.new + q = IssueQuery.new q.column_names = ['tracker', :subject, '', 'unknonw_column'] - assert_equal [:tracker, :subject], q.columns.collect {|c| c.name} - c = q.columns.first - assert q.has_column?(c) + assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name} + end + + def test_has_column_should_accept_a_column_name + q = IssueQuery.new + q.column_names = ['tracker', :subject] + assert q.has_column?(:tracker) + assert !q.has_column?(:category) + end + + def test_has_column_should_accept_a_column + q = IssueQuery.new + q.column_names = ['tracker', :subject] + + tracker_column = q.available_columns.detect {|c| c.name==:tracker} + assert_kind_of QueryColumn, tracker_column + category_column = q.available_columns.detect {|c| c.name==:category} + assert_kind_of QueryColumn, category_column + + assert q.has_column?(tracker_column) + assert !q.has_column?(category_column) end def test_inline_and_block_columns - q = Query.new + q = IssueQuery.new q.column_names = ['subject', 'description', 'tracker'] - assert_equal [:subject, :tracker], q.inline_columns.map(&:name) + assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name) assert_equal [:description], q.block_columns.map(&:name) end def test_custom_field_columns_should_be_inline - q = Query.new + q = IssueQuery.new columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn} assert columns.any? assert_nil columns.detect {|column| !column.inline?} end def test_query_should_preload_spent_hours - q = Query.new(:name => '_', :column_names => [:subject, :spent_hours]) + q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours]) assert q.has_column?(:spent_hours) issues = q.issues assert_not_nil issues.first.instance_variable_get("@spent_hours") end def test_groupable_columns_should_include_custom_fields - q = Query.new + q = IssueQuery.new column = q.groupable_columns.detect {|c| c.name == :cf_1} assert_not_nil column assert_kind_of QueryCustomFieldColumn, column @@ -783,7 +921,7 @@ field = CustomField.find(1) field.update_attribute :multiple, true - q = Query.new + q = IssueQuery.new column = q.groupable_columns.detect {|c| c.name == :cf_1} assert_nil column end @@ -791,19 +929,19 @@ def test_groupable_columns_should_include_user_custom_fields cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user') - q = Query.new + q = IssueQuery.new assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym} end def test_groupable_columns_should_include_version_custom_fields cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version') - q = Query.new + q = IssueQuery.new assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym} end def test_grouped_with_valid_column - q = Query.new(:group_by => 'status') + q = IssueQuery.new(:group_by => 'status') assert q.grouped? assert_not_nil q.group_by_column assert_equal :status, q.group_by_column.name @@ -812,30 +950,30 @@ end def test_grouped_with_invalid_column - q = Query.new(:group_by => 'foo') + q = IssueQuery.new(:group_by => 'foo') assert !q.grouped? assert_nil q.group_by_column assert_nil q.group_by_statement end - + def test_sortable_columns_should_sort_assignees_according_to_user_format_setting with_settings :user_format => 'lastname_coma_firstname' do - q = Query.new + q = IssueQuery.new assert q.sortable_columns.has_key?('assigned_to') assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to'] end end - + def test_sortable_columns_should_sort_authors_according_to_user_format_setting with_settings :user_format => 'lastname_coma_firstname' do - q = Query.new + q = IssueQuery.new assert q.sortable_columns.has_key?('author') assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author'] end end def test_sortable_columns_should_include_custom_field - q = Query.new + q = IssueQuery.new assert q.sortable_columns['cf_1'] end @@ -843,29 +981,29 @@ field = CustomField.find(1) field.update_attribute :multiple, true - q = Query.new + q = IssueQuery.new assert !q.sortable_columns['cf_1'] end def test_default_sort - q = Query.new + q = IssueQuery.new assert_equal [], q.sort_criteria end def test_set_sort_criteria_with_hash - q = Query.new + q = IssueQuery.new q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']} assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria end def test_set_sort_criteria_with_array - q = Query.new + q = IssueQuery.new q.sort_criteria = [['priority', 'desc'], 'tracker'] assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria end def test_create_query_with_sort - q = Query.new(:name => 'Sorted') + q = IssueQuery.new(:name => 'Sorted') q.sort_criteria = [['priority', 'desc'], 'tracker'] assert q.save q.reload @@ -873,53 +1011,47 @@ end def test_sort_by_string_custom_field_asc - q = Query.new + q = IssueQuery.new c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' } assert c assert c.sortable - issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( - q.statement - ).order("#{c.sortable} ASC").all + issues = q.issues(:order => "#{c.sortable} ASC") values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s} assert !values.empty? assert_equal values.sort, values end def test_sort_by_string_custom_field_desc - q = Query.new + q = IssueQuery.new c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' } assert c assert c.sortable - issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( - q.statement - ).order("#{c.sortable} DESC").all + issues = q.issues(:order => "#{c.sortable} DESC") values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s} assert !values.empty? assert_equal values.sort.reverse, values end def test_sort_by_float_custom_field_asc - q = Query.new + q = IssueQuery.new c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' } assert c assert c.sortable - issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( - q.statement - ).order("#{c.sortable} ASC").all + issues = q.issues(:order => "#{c.sortable} ASC") values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact assert !values.empty? assert_equal values.sort, values end def test_invalid_query_should_raise_query_statement_invalid_error - q = Query.new + q = IssueQuery.new assert_raise Query::StatementInvalid do q.issues(:conditions => "foo = 1") end end def test_issue_count - q = Query.new(:name => '_') + q = IssueQuery.new(:name => '_') issue_count = q.issue_count assert_equal q.issues.size, issue_count end @@ -935,7 +1067,7 @@ end def test_issue_count_by_association_group - q = Query.new(:name => '_', :group_by => 'assigned_to') + q = IssueQuery.new(:name => '_', :group_by => 'assigned_to') count_by_group = q.issue_count_by_group assert_kind_of Hash, count_by_group assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort @@ -944,7 +1076,7 @@ end def test_issue_count_by_list_custom_field_group - q = Query.new(:name => '_', :group_by => 'cf_1') + q = IssueQuery.new(:name => '_', :group_by => 'cf_1') count_by_group = q.issue_count_by_group assert_kind_of Hash, count_by_group assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort @@ -953,7 +1085,7 @@ end def test_issue_count_by_date_custom_field_group - q = Query.new(:name => '_', :group_by => 'cf_8') + q = IssueQuery.new(:name => '_', :group_by => 'cf_8') count_by_group = q.issue_count_by_group assert_kind_of Hash, count_by_group assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort @@ -963,7 +1095,7 @@ def test_issue_count_with_nil_group_only Issue.update_all("assigned_to_id = NULL") - q = Query.new(:name => '_', :group_by => 'assigned_to') + q = IssueQuery.new(:name => '_', :group_by => 'assigned_to') count_by_group = q.issue_count_by_group assert_kind_of Hash, count_by_group assert_equal 1, count_by_group.keys.size @@ -971,7 +1103,7 @@ end def test_issue_ids - q = Query.new(:name => '_') + q = IssueQuery.new(:name => '_') order = "issues.subject, issues.id" issues = q.issues(:order => order) assert_equal issues.map(&:id), q.issue_ids(:order => order) @@ -979,13 +1111,13 @@ def test_label_for set_language_if_valid 'en' - q = Query.new + q = IssueQuery.new assert_equal 'Assignee', q.label_for('assigned_to_id') end def test_label_for_fr set_language_if_valid 'fr' - q = Query.new + q = IssueQuery.new s = "Assign\xc3\xa9 \xc3\xa0" s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) assert_equal s, q.label_for('assigned_to_id') @@ -997,32 +1129,32 @@ developer = User.find(3) # Public query on project 1 - q = Query.find(1) + q = IssueQuery.find(1) assert q.editable_by?(admin) assert q.editable_by?(manager) assert !q.editable_by?(developer) # Private query on project 1 - q = Query.find(2) + q = IssueQuery.find(2) assert q.editable_by?(admin) assert !q.editable_by?(manager) assert q.editable_by?(developer) # Private query for all projects - q = Query.find(3) + q = IssueQuery.find(3) assert q.editable_by?(admin) assert !q.editable_by?(manager) assert q.editable_by?(developer) # Public query for all projects - q = Query.find(4) + q = IssueQuery.find(4) assert q.editable_by?(admin) assert !q.editable_by?(manager) assert !q.editable_by?(developer) end def test_visible_scope - query_ids = Query.visible(User.anonymous).map(&:id) + query_ids = IssueQuery.visible(User.anonymous).map(&:id) assert query_ids.include?(1), 'public query on public project was not visible' assert query_ids.include?(4), 'public query for all projects was not visible' @@ -1031,81 +1163,120 @@ assert !query_ids.include?(7), 'public query on private project was visible' end - context "#available_filters" do - setup do - @query = Query.new(:name => "_") + def test_query_with_public_visibility_should_be_visible_to_anyone + q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC) + + assert q.visible?(User.anonymous) + assert IssueQuery.visible(User.anonymous).find_by_id(q.id) + + assert q.visible?(User.find(7)) + assert IssueQuery.visible(User.find(7)).find_by_id(q.id) + + assert q.visible?(User.find(2)) + assert IssueQuery.visible(User.find(2)).find_by_id(q.id) + + assert q.visible?(User.find(1)) + assert IssueQuery.visible(User.find(1)).find_by_id(q.id) + end + + def test_query_with_roles_visibility_should_be_visible_to_user_with_role + q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2]) + + assert !q.visible?(User.anonymous) + assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id) + + assert !q.visible?(User.find(7)) + assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id) + + assert q.visible?(User.find(2)) + assert IssueQuery.visible(User.find(2)).find_by_id(q.id) + + assert q.visible?(User.find(1)) + assert IssueQuery.visible(User.find(1)).find_by_id(q.id) + end + + def test_query_with_private_visibility_should_be_visible_to_owner + q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7)) + + assert !q.visible?(User.anonymous) + assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id) + + assert q.visible?(User.find(7)) + assert IssueQuery.visible(User.find(7)).find_by_id(q.id) + + assert !q.visible?(User.find(2)) + assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id) + + assert q.visible?(User.find(1)) + assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id) + end + + test "#available_filters should include users of visible projects in cross-project view" do + users = IssueQuery.new.available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?("3") + end + + test "#available_filters should include users of subprojects" do + user1 = User.generate! + user2 = User.generate! + project = Project.find(1) + Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1]) + + users = IssueQuery.new(:project => project).available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?(user1.id.to_s) + assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s) + end + + test "#available_filters should include visible projects in cross-project view" do + projects = IssueQuery.new.available_filters["project_id"] + assert_not_nil projects + assert projects[:values].map{|u|u[1]}.include?("1") + end + + test "#available_filters should include 'member_of_group' filter" do + query = IssueQuery.new + assert query.available_filters.keys.include?("member_of_group") + assert_equal :list_optional, query.available_filters["member_of_group"][:type] + assert query.available_filters["member_of_group"][:values].present? + assert_equal Group.all.sort.map {|g| [g.name, g.id.to_s]}, + query.available_filters["member_of_group"][:values].sort + end + + test "#available_filters should include 'assigned_to_role' filter" do + query = IssueQuery.new + assert query.available_filters.keys.include?("assigned_to_role") + assert_equal :list_optional, query.available_filters["assigned_to_role"][:type] + + assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1']) + assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2']) + assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3']) + + assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4']) + assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5']) + end + + def test_available_filters_should_include_custom_field_according_to_user_visibility + visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true) + hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1]) + + with_current_user User.find(3) do + query = IssueQuery.new + assert_include "cf_#{visible_field.id}", query.available_filters.keys + assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys end + end - should "include users of visible projects in cross-project view" do - users = @query.available_filters["assigned_to_id"] - assert_not_nil users - assert users[:values].map{|u|u[1]}.include?("3") + def test_available_columns_should_include_custom_field_according_to_user_visibility + visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true) + hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1]) + + with_current_user User.find(3) do + query = IssueQuery.new + assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name) + assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name) end - - should "include users of subprojects" do - user1 = User.generate! - user2 = User.generate! - project = Project.find(1) - Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1]) - @query.project = project - - users = @query.available_filters["assigned_to_id"] - assert_not_nil users - assert users[:values].map{|u|u[1]}.include?(user1.id.to_s) - assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s) - end - - should "include visible projects in cross-project view" do - projects = @query.available_filters["project_id"] - assert_not_nil projects - assert projects[:values].map{|u|u[1]}.include?("1") - end - - context "'member_of_group' filter" do - should "be present" do - assert @query.available_filters.keys.include?("member_of_group") - end - - should "be an optional list" do - assert_equal :list_optional, @query.available_filters["member_of_group"][:type] - end - - should "have a list of the groups as values" do - Group.destroy_all # No fixtures - group1 = Group.generate!.reload - group2 = Group.generate!.reload - - expected_group_list = [ - [group1.name, group1.id.to_s], - [group2.name, group2.id.to_s] - ] - assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort - end - - end - - context "'assigned_to_role' filter" do - should "be present" do - assert @query.available_filters.keys.include?("assigned_to_role") - end - - should "be an optional list" do - assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type] - end - - should "have a list of the Roles as values" do - assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1']) - assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2']) - assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3']) - end - - should "not include the built in Roles as values" do - assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4']) - assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5']) - end - - end - end context "#statement" do @@ -1127,34 +1298,34 @@ end should "search assigned to for users in the group" do - @query = Query.new(:name => '_') + @query = IssueQuery.new(:name => '_') @query.add_filter('member_of_group', '=', [@group.id.to_s]) - assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')" + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@group.id}')" assert_find_issues_with_query_is_successful @query end should "search not assigned to any group member (none)" do - @query = Query.new(:name => '_') + @query = IssueQuery.new(:name => '_') @query.add_filter('member_of_group', '!*', ['']) # Users not in a group - assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')" + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')" assert_find_issues_with_query_is_successful @query end should "search assigned to any group member (all)" do - @query = Query.new(:name => '_') + @query = IssueQuery.new(:name => '_') @query.add_filter('member_of_group', '*', ['']) # Only users in a group - assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')" + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}','#{@group.id}','#{@group2.id}')" assert_find_issues_with_query_is_successful @query end should "return an empty set with = empty group" do @empty_group = Group.generate! - @query = Query.new(:name => '_') + @query = IssueQuery.new(:name => '_') @query.add_filter('member_of_group', '=', [@empty_group.id.to_s]) assert_equal [], find_issues_with_query(@query) @@ -1162,7 +1333,7 @@ should "return issues with ! empty group" do @empty_group = Group.generate! - @query = Query.new(:name => '_') + @query = IssueQuery.new(:name => '_') @query.add_filter('member_of_group', '!', [@empty_group.id.to_s]) assert_find_issues_with_query_is_successful @query @@ -1182,7 +1353,7 @@ User.add_to_project(@manager, @project, @manager_role) User.add_to_project(@developer, @project, @developer_role) User.add_to_project(@boss, @project, [@manager_role, @developer_role]) - + @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id) @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id) @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id) @@ -1191,7 +1362,7 @@ end should "search assigned to for users with the Role" do - @query = Query.new(:name => '_', :project => @project) + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) assert_query_result [@issue1, @issue3], @query @@ -1200,8 +1371,8 @@ should "search assigned to for users with the Role on the issue project" do other_project = Project.generate! User.add_to_project(@developer, other_project, @manager_role) - - @query = Query.new(:name => '_', :project => @project) + + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) assert_query_result [@issue1, @issue3], @query @@ -1209,28 +1380,28 @@ should "return an empty set with empty role" do @empty_role = Role.generate! - @query = Query.new(:name => '_', :project => @project) + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s]) assert_query_result [], @query end should "search assigned to for users without the Role" do - @query = Query.new(:name => '_', :project => @project) + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s]) assert_query_result [@issue2, @issue4, @issue5], @query end should "search assigned to for users not assigned to any Role (none)" do - @query = Query.new(:name => '_', :project => @project) + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '!*', ['']) assert_query_result [@issue4, @issue5], @query end should "search assigned to for users assigned to any Role (all)" do - @query = Query.new(:name => '_', :project => @project) + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '*', ['']) assert_query_result [@issue1, @issue2, @issue3], @query @@ -1238,12 +1409,11 @@ should "return issues with ! empty role" do @empty_role = Role.generate! - @query = Query.new(:name => '_', :project => @project) + @query = IssueQuery.new(:name => '_', :project => @project) @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s]) assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query end end end - end diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_bazaar_test.rb --- a/test/unit/repository_bazaar_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_bazaar_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -105,8 +105,9 @@ @project.reload assert_equal NUM_REV, @repository.changesets.count # Remove changesets with revision > 5 - @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 2} @project.reload + @repository.reload assert_equal 2, @repository.changesets.count @repository.fetch_changesets diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_cvs_test.rb --- a/test/unit/repository_cvs_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_cvs_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -116,12 +116,13 @@ assert_equal CHANGESETS_NUM, @repository.changesets.count # Remove changesets with revision > 3 - @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3} + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 3} @project.reload + @repository.reload assert_equal 3, @repository.changesets.count - assert_equal %w|3 2 1|, @repository.changesets.all.collect(&:revision) + assert_equal %w|3 2 1|, @repository.changesets.collect(&:revision) - rev3_commit = @repository.changesets.find(:first, :order => 'committed_on DESC') + rev3_commit = @repository.changesets.reorder('committed_on DESC').first assert_equal '3', rev3_commit.revision # 2007-12-14 01:27:22 +0900 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22) @@ -132,8 +133,9 @@ @repository.fetch_changesets @project.reload + @repository.reload assert_equal CHANGESETS_NUM, @repository.changesets.count - assert_equal %w|7 6 5 4 3 2 1|, @repository.changesets.all.collect(&:revision) + assert_equal %w|7 6 5 4 3 2 1|, @repository.changesets.collect(&:revision) rev5_commit = @repository.changesets.find_by_revision('5') assert_equal 'HEAD-20071213-163001', rev5_commit.scmid # 2007-12-14 01:30:01 +0900 diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_darcs_test.rb --- a/test/unit/repository_darcs_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_darcs_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -79,8 +79,9 @@ assert_equal NUM_REV, @repository.changesets.count # Remove changesets with revision > 3 - @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3} + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 3} @project.reload + @repository.reload assert_equal 3, @repository.changesets.count @repository.fetch_changesets diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_filesystem_test.rb --- a/test/unit/repository_filesystem_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_filesystem_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_git_test.rb --- a/test/unit/repository_git_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_git_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_mercurial_test.rb --- a/test/unit/repository_mercurial_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_mercurial_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -23,7 +23,7 @@ include Redmine::I18n REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s - NUM_REV = 32 + NUM_REV = 34 CHAR_1_HEX = "\xc3\x9c" def setup @@ -46,7 +46,6 @@ end end - def test_blank_path_to_repository_error_message set_language_if_valid 'en' repo = Repository::Mercurial.new( @@ -81,19 +80,138 @@ assert_equal true, klass.scm_available end - def test_entries + def test_entries_on_tip entries = @repository.entries assert_kind_of Redmine::Scm::Adapters::Entries, entries end + def assert_entries(is_short_scmid=true) + hex = "9d5b5b00419901478496242e0768deba1ce8c51e" + scmid = scmid_for_assert(hex, is_short_scmid) + [2, '400bb8672109', '400', 400].each do |r| + entries1 = @repository.entries(nil, r) + assert entries1 + assert_kind_of Redmine::Scm::Adapters::Entries, entries1 + assert_equal 3, entries1.size + readme = entries1[2] + assert_equal '1', readme.lastrev.revision + assert_equal scmid, readme.lastrev.identifier + assert_equal '1', readme.changeset.revision + assert_equal scmid, readme.changeset.scmid + end + end + private :assert_entries + + def test_entries_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_entries(true) + end + + def test_entries_long_id + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_entries(false) + end + + def test_entry_on_tip + entry = @repository.entry + assert_kind_of Redmine::Scm::Adapters::Entry, entry + assert_equal "", entry.path + assert_equal 'dir', entry.kind + end + + def assert_entry(is_short_scmid=true) + hex = "0885933ad4f68d77c2649cd11f8311276e7ef7ce" + scmid = scmid_for_assert(hex, is_short_scmid) + ["README", "/README"].each do |path| + ["0", "0885933ad4f6", "0885933ad4f68d77c2649cd11f8311276e7ef7ce"].each do |rev| + entry = @repository.entry(path, rev) + assert_kind_of Redmine::Scm::Adapters::Entry, entry + assert_equal "README", entry.path + assert_equal "file", entry.kind + assert_equal '0', entry.lastrev.revision + assert_equal scmid, entry.lastrev.identifier + end + end + ["sources", "/sources", "/sources/"].each do |path| + ["0", "0885933ad4f6", "0885933ad4f68d77c2649cd11f8311276e7ef7ce"].each do |rev| + entry = @repository.entry(path, rev) + assert_kind_of Redmine::Scm::Adapters::Entry, entry + assert_equal "sources", entry.path + assert_equal "dir", entry.kind + end + end + ["sources/watchers_controller.rb", "/sources/watchers_controller.rb"].each do |path| + ["0", "0885933ad4f6", "0885933ad4f68d77c2649cd11f8311276e7ef7ce"].each do |rev| + entry = @repository.entry(path, rev) + assert_kind_of Redmine::Scm::Adapters::Entry, entry + assert_equal "sources/watchers_controller.rb", entry.path + assert_equal "file", entry.kind + assert_equal '0', entry.lastrev.revision + assert_equal scmid, entry.lastrev.identifier + end + end + end + private :assert_entry + + def test_entry_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + assert_entry(true) + end + + def test_entry_long_id + assert_entry(false) + end + def test_fetch_changesets_from_scratch assert_equal 0, @repository.changesets.count @repository.fetch_changesets @project.reload assert_equal NUM_REV, @repository.changesets.count assert_equal 46, @repository.filechanges.count + rev0 = @repository.changesets.find_by_revision('0') assert_equal "Initial import.\nThe repository contains 3 files.", - @repository.changesets.find_by_revision('0').comments + rev0.comments + assert_equal "0885933ad4f68d77c2649cd11f8311276e7ef7ce", rev0.scmid + first_rev = @repository.changesets.first + last_rev = @repository.changesets.last + assert_equal "#{NUM_REV - 1}", first_rev.revision + assert_equal "0", last_rev.revision + end + + def test_fetch_changesets_keep_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + rev1 = @repository.changesets.find_by_revision('1') + assert_equal "9d5b5b004199", rev1.scmid + end + + def test_fetch_changesets_keep_long_id + assert_equal 0, @repository.changesets.count + Changeset.create!(:repository => @repository, + :committed_on => Time.now, + :revision => '0', + :scmid => '0885933ad4f68d77c2649cd11f8311276e7ef7ce', + :comments => 'test') + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + rev1 = @repository.changesets.find_by_revision('1') + assert_equal "9d5b5b00419901478496242e0768deba1ce8c51e", rev1.scmid end def test_fetch_changesets_incremental @@ -102,8 +220,9 @@ @project.reload assert_equal NUM_REV, @repository.changesets.count # Remove changesets with revision > 2 - @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 2} @project.reload + @repository.reload assert_equal 3, @repository.changesets.count @repository.fetch_changesets @@ -144,7 +263,7 @@ # with_limit changesets = @repository.latest_changesets('', nil, 2) - assert_equal %w|31 30|, changesets.collect(&:revision) + assert_equal ["#{NUM_REV - 1}", "#{NUM_REV - 2}"], changesets.collect(&:revision) # with_filepath changesets = @repository.latest_changesets( @@ -187,21 +306,61 @@ changesets = @repository.latest_changesets(path, '12', 1) assert_equal %w|12|, changesets.collect(&:revision) + end - # tag + def assert_latest_changesets_tag changesets = @repository.latest_changesets('', 'tag_test.00') assert_equal %w|5 4 3 2 1 0|, changesets.collect(&:revision) + end + private :assert_latest_changesets_tag + + def test_latest_changesets_tag + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_latest_changesets_tag + end + + def test_latest_changesets_tag_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_latest_changesets_tag + end + + def test_latest_changesets_tag_with_path + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + changesets = @repository.latest_changesets('sources', 'tag_test.00') + assert_equal %w|4 3 2 1 0|, changesets.collect(&:revision) + end + + def test_latest_changesets_tag_with_limit + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count changesets = @repository.latest_changesets('', 'tag_test.00', 2) assert_equal %w|5 4|, changesets.collect(&:revision) - changesets = @repository.latest_changesets('sources', 'tag_test.00') - assert_equal %w|4 3 2 1 0|, changesets.collect(&:revision) - changesets = @repository.latest_changesets('sources', 'tag_test.00', 2) assert_equal %w|4 3|, changesets.collect(&:revision) + end - # named branch + def test_latest_changesets_branch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + if @repository.scm.class.client_version_above?([1, 6]) changesets = @repository.latest_changesets('', @branch_char_1) assert_equal %w|27 26|, changesets.collect(&:revision) @@ -211,21 +370,42 @@ assert_equal %w|27|, changesets.collect(&:revision) end - def test_copied_files + def assert_latest_changesets_default_branch + changesets = @repository.latest_changesets('', 'default') + assert_equal %w|31 28 24 6 4 3 2 1 0|, changesets.collect(&:revision) + end + private :assert_latest_changesets_default_branch + + def test_latest_changesets_default_branch assert_equal 0, @repository.changesets.count @repository.fetch_changesets @project.reload assert_equal NUM_REV, @repository.changesets.count + assert_latest_changesets_default_branch + end + def test_latest_changesets_default_branch_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_latest_changesets_default_branch + end + + def assert_copied_files(is_short_scmid=true) cs1 = @repository.changesets.find_by_revision('13') assert_not_nil cs1 c1 = cs1.filechanges.sort_by(&:path) assert_equal 2, c1.size + hex1 = "3a330eb329586ea2adb3f83237c23310e744ebe9" + scmid1 = scmid_for_assert(hex1, is_short_scmid) assert_equal 'A', c1[0].action assert_equal '/sql_escape/percent%dir/percentfile1.txt', c1[0].path assert_equal '/sql_escape/percent%dir/percent%file1.txt', c1[0].from_path - assert_equal '3a330eb32958', c1[0].from_revision + assert_equal scmid1, c1[0].from_revision assert_equal 'A', c1[1].action assert_equal '/sql_escape/underscore_dir/understrike-file.txt', c1[1].path @@ -235,18 +415,42 @@ c2 = cs2.filechanges assert_equal 1, c2.size + hex2 = "933ca60293d78f7c7979dd123cc0c02431683575" + scmid2 = scmid_for_assert(hex2, is_short_scmid) assert_equal 'A', c2[0].action assert_equal '/README (1)[2]&,%.-3_4', c2[0].path assert_equal '/README', c2[0].from_path - assert_equal '933ca60293d7', c2[0].from_revision + assert_equal scmid2, c2[0].from_revision cs3 = @repository.changesets.find_by_revision('19') c3 = cs3.filechanges + + hex3 = "5d9891a1b4258ea256552aa856e388f2da28256a" + scmid3 = scmid_for_assert(hex3, is_short_scmid) assert_equal 1, c3.size assert_equal 'A', c3[0].action assert_equal "/latin-1-dir/test-#{@char_1}-1.txt", c3[0].path assert_equal "/latin-1-dir/test-#{@char_1}.txt", c3[0].from_path - assert_equal '5d9891a1b425', c3[0].from_revision + assert_equal scmid3, c3[0].from_revision + end + private :assert_copied_files + + def test_copied_files_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_copied_files(true) + end + + def test_copied_files_long_id + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_copied_files(false) end def test_find_changeset_by_name @@ -285,6 +489,17 @@ assert_equal '2:400bb8672109', c.format_identifier end + def test_format_identifier_long_id + assert_equal 0, @repository.changesets.count + Changeset.create!(:repository => @repository, + :committed_on => Time.now, + :revision => '0', + :scmid => '0885933ad4f68d77c2649cd11f8311276e7ef7ce', + :comments => 'test') + c = @repository.changesets.find_by_revision('0') + assert_equal '0:0885933ad4f6', c.format_identifier + end + def test_find_changeset_by_empty_name assert_equal 0, @repository.changesets.count @repository.fetch_changesets @@ -295,22 +510,42 @@ end end - def test_parents + def assert_parents(is_short_scmid=true) + r1 = @repository.changesets.find_by_revision('0') + assert_equal [], r1.parents + r2 = @repository.changesets.find_by_revision('1') + hex2 = "0885933ad4f68d77c2649cd11f8311276e7ef7ce" + scmid2 = scmid_for_assert(hex2, is_short_scmid) + assert_equal 1, r2.parents.length + assert_equal scmid2, r2.parents[0].identifier + r3 = @repository.changesets.find_by_revision('30') + assert_equal 2, r3.parents.length + r4 = [r3.parents[0].identifier, r3.parents[1].identifier].sort + hex41 = "3a330eb329586ea2adb3f83237c23310e744ebe9" + scmid41 = scmid_for_assert(hex41, is_short_scmid) + hex42 = "a94b0528f24fe05ebaef496ae0500bb050772e36" + scmid42 = scmid_for_assert(hex42, is_short_scmid) + assert_equal scmid41, r4[0] + assert_equal scmid42, r4[1] + end + private :assert_parents + + def test_parents_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_parents(true) + end + + def test_parents_long_id assert_equal 0, @repository.changesets.count @repository.fetch_changesets @project.reload assert_equal NUM_REV, @repository.changesets.count - r1 = @repository.changesets.find_by_revision('0') - assert_equal [], r1.parents - r2 = @repository.changesets.find_by_revision('1') - assert_equal 1, r2.parents.length - assert_equal "0885933ad4f6", - r2.parents[0].identifier - r3 = @repository.changesets.find_by_revision('30') - assert_equal 2, r3.parents.length - r4 = [r3.parents[0].identifier, r3.parents[1].identifier].sort - assert_equal "3a330eb32958", r4[0] - assert_equal "a94b0528f24f", r4[1] + assert_parents(false) end def test_activities @@ -365,11 +600,52 @@ @repository.fetch_changesets @project.reload assert_equal NUM_REV, @repository.changesets.count - %w|31 31eeee7395c8 31eee|.each do |r1| + ["#{NUM_REV - 1}", "2e6d54642923", "2e6d5"].each do |r1| changeset = @repository.find_changeset_by_name(r1) assert_nil changeset.next end end + + def test_scmid_for_inserting_db_short_id + assert_equal 0, @repository.changesets.count + create_rev0_short_id + assert_equal 1, @repository.changesets.count + rev = "0123456789012345678901234567890123456789" + assert_equal 12, @repository.scmid_for_inserting_db(rev).length + end + + def test_scmid_for_inserting_db_long_id + rev = "0123456789012345678901234567890123456789" + assert_equal 0, @repository.changesets.count + assert_equal 40, @repository.scmid_for_inserting_db(rev).length + Changeset.create!(:repository => @repository, + :committed_on => Time.now, + :revision => '0', + :scmid => rev, + :comments => 'test') + assert_equal 1, @repository.changesets.count + assert_equal 40, @repository.scmid_for_inserting_db(rev).length + end + + def test_scmid_for_assert + rev = "0123456789012345678901234567890123456789" + assert_equal rev, scmid_for_assert(rev, false) + assert_equal "012345678901", scmid_for_assert(rev, true) + end + + private + + def scmid_for_assert(hex, is_short=true) + is_short ? hex[0, 12] : hex + end + + def create_rev0_short_id + Changeset.create!(:repository => @repository, + :committed_on => Time.now, + :revision => '0', + :scmid => '0885933ad4f6', + :comments => 'test') + end else puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_subversion_test.rb --- a/test/unit/repository_subversion_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_subversion_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -20,15 +20,43 @@ class RepositorySubversionTest < ActiveSupport::TestCase fixtures :projects, :repositories, :enabled_modules, :users, :roles + include Redmine::I18n + NUM_REV = 11 def setup @project = Project.find(3) @repository = Repository::Subversion.create(:project => @project, - :url => self.class.subversion_repository_url) + :url => self.class.subversion_repository_url) assert @repository end + def test_invalid_url + set_language_if_valid 'en' + ['invalid', 'http://', 'svn://', 'svn+ssh://', 'file://'].each do |url| + repo = Repository::Subversion.new( + :project => @project, + :identifier => 'test', + :url => url + ) + assert !repo.save + assert_equal ["is invalid"], repo.errors[:url] + end + end + + def test_valid_url + ['http://valid', 'svn://valid', 'svn+ssh://valid', 'file://valid'].each do |url| + repo = Repository::Subversion.new( + :project => @project, + :identifier => 'test', + :url => url + ) + assert repo.save + assert_equal [], repo.errors[:url] + assert repo.destroy + end + end + if repository_configured?('subversion') def test_fetch_changesets_from_scratch assert_equal 0, @repository.changesets.count @@ -47,8 +75,9 @@ assert_equal NUM_REV, @repository.changesets.count # Remove changesets with revision > 5 - @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5} + @repository.changesets.each {|c| c.destroy if c.revision.to_i > 5} @project.reload + @repository.reload assert_equal 5, @repository.changesets.count @repository.fetch_changesets diff -r d98d22a98252 -r fb9a13467253 test/unit/repository_test.rb --- a/test/unit/repository_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/repository_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -97,6 +97,31 @@ assert_equal [repository1, repository2], Project.find(3).repositories.sort end + def test_default_repository_should_be_one + assert_equal 0, Project.find(3).repositories.count + repository1 = Repository::Subversion.new( + :project => Project.find(3), + :identifier => 'svn1', + :url => 'file:///svn1' + ) + assert repository1.save + assert repository1.is_default? + + repository2 = Repository::Subversion.new( + :project => Project.find(3), + :identifier => 'svn2', + :url => 'file:///svn2', + :is_default => true + ) + assert repository2.save + assert repository2.is_default? + repository1.reload + assert !repository1.is_default? + + assert_equal repository2, Project.find(3).repository + assert_equal [repository2, repository1], Project.find(3).repositories.sort + end + def test_identifier_should_accept_letters_digits_dashes_and_underscores r = Repository::Subversion.new( :project_id => 3, @@ -105,20 +130,18 @@ ) assert r.save end - + def test_identifier_should_not_be_frozen_for_a_new_repository assert_equal false, Repository.new.identifier_frozen? end def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier - Repository.update_all(["identifier = ''"], "id = 10") - + Repository.where(:id => 10).update_all(["identifier = ''"]) assert_equal false, Repository.find(10).identifier_frozen? end def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier - Repository.update_all(["identifier = 'abc123'"], "id = 10") - + Repository.where(:id => 10).update_all(["identifier = 'abc123'"]) assert_equal true, Repository.find(10).identifier_frozen? end @@ -152,18 +175,16 @@ def test_destroy_should_delete_parents_associations changeset = Changeset.find(102) - changeset.parents = Changeset.find_all_by_id([100, 101]) - - assert_difference 'Changeset.connection.select_all("select * from changeset_parents").size', -2 do + changeset.parents = Changeset.where(:id => [100, 101]).all + assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do Repository.find(10).destroy end end def test_destroy_should_delete_issues_associations changeset = Changeset.find(102) - changeset.issues = Issue.find_all_by_id([1, 2]) - - assert_difference 'Changeset.connection.select_all("select * from changesets_issues").size', -2 do + changeset.issues = Issue.where(:id => [1, 2]).all + assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do Repository.find(10).destroy end end @@ -181,14 +202,12 @@ def test_scan_changesets_for_issue_ids Setting.default_language = 'en' - - # choosing a status to apply to fix issues - Setting.commit_fix_status_id = IssueStatus.find( - :first, - :conditions => ["is_closed = ?", true]).id - Setting.commit_fix_done_ratio = "90" Setting.commit_ref_keywords = 'refs , references, IssueID' - Setting.commit_fix_keywords = 'fixes , closes' + Setting.commit_update_keywords = [ + {'keywords' => 'fixes , closes', + 'status_id' => IssueStatus.where(:is_closed => true).first.id, + 'done_ratio' => '90'} + ] Setting.default_language = 'en' ActionMailer::Base.deliveries.clear @@ -209,7 +228,7 @@ assert_equal [101], fixed_issue.changeset_ids # issue change - journal = fixed_issue.journals.find(:first, :order => 'created_on desc') + journal = fixed_issue.journals.reorder('created_on desc').first assert_equal User.find_by_login('dlopper'), journal.user assert_equal 'Applied in changeset r2.', journal.notes @@ -278,7 +297,7 @@ end def test_manual_user_mapping - assert_no_difference "Changeset.count(:conditions => 'user_id <> 2')" do + assert_no_difference "Changeset.where('user_id <> 2').count" do c = Changeset.create!( :repository => @repository, :committer => 'foo', @@ -329,6 +348,12 @@ assert_equal true, klass.scm_available end + def test_extra_info_should_not_return_non_hash_value + repo = Repository.new + repo.extra_info = "foo" + assert_nil repo.extra_info + end + def test_merge_extra_info repo = Repository::Subversion.new(:project => Project.find(3)) assert !repo.save diff -r d98d22a98252 -r fb9a13467253 test/unit/role_test.rb --- a/test/unit/role_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/role_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -91,55 +91,39 @@ assert_equal Role.all.reject(&:builtin?).sort, Role.find_all_givable end - context "#anonymous" do - should "return the anonymous role" do + 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 - context "with a missing anonymous role" do - setup do - Role.delete_all("builtin = #{Role::BUILTIN_ANONYMOUS}") - end + def test_anonymous_with_a_missing_anonymous_role_should_return_the_anonymous_role + Role.where(:builtin => Role::BUILTIN_ANONYMOUS).delete_all - should "create a new anonymous role" do - assert_difference('Role.count') do - Role.anonymous - end - end - - should "return the anonymous role" do - role = Role.anonymous - assert role.builtin? - assert_equal Role::BUILTIN_ANONYMOUS, role.builtin - end + assert_difference('Role.count') do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin end end - context "#non_member" do - should "return the non-member role" do + 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 - context "with a missing non-member role" do - setup do - Role.delete_all("builtin = #{Role::BUILTIN_NON_MEMBER}") - 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 - should "create a new non-member role" do - assert_difference('Role.count') do - Role.non_member - end - end - - should "return the non-member role" do - role = Role.non_member - assert role.builtin? - assert_equal Role::BUILTIN_NON_MEMBER, role.builtin - end + 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 d98d22a98252 -r fb9a13467253 test/unit/search_test.rb --- a/test/unit/search_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/search_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -49,6 +49,7 @@ # Removes the :view_changesets permission from Anonymous role remove_permission Role.anonymous, :view_changesets + User.current = nil r = Issue.search(@issue_keyword).first assert r.include?(@issue) @@ -74,6 +75,7 @@ # Removes the :view_changesets permission from Non member role remove_permission Role.non_member, :view_changesets + User.current = User.find_by_login('rhill') r = Issue.search(@issue_keyword).first assert r.include?(@issue) @@ -128,7 +130,7 @@ def test_search_issue_with_multiple_hits_in_journals i = Issue.find(1) - assert_equal 2, i.journals.count(:all, :conditions => "notes LIKE '%notes%'") + assert_equal 2, i.journals.where("notes LIKE '%notes%'").count r = Issue.search('%notes%').first assert_equal 1, r.size diff -r d98d22a98252 -r fb9a13467253 test/unit/setting_test.rb --- a/test/unit/setting_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/setting_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -41,21 +41,35 @@ assert_equal "My other title", Setting.find_by_name('app_title').value end + def test_setting_with_int_format_should_accept_numeric_only + with_settings :session_timeout => 30 do + Setting.session_timeout = 'foo' + assert_equal "30", Setting.session_timeout + Setting.session_timeout = 40 + assert_equal "40", Setting.session_timeout + end + end + + def test_setting_with_invalid_name_should_be_valid + setting = Setting.new(:name => "does_not_exist", :value => "should_not_be_allowed") + assert !setting.save + end + def test_serialized_setting Setting.notified_events = ['issue_added', 'issue_updated', 'news_added'] assert_equal ['issue_added', 'issue_updated', 'news_added'], Setting.notified_events assert_equal ['issue_added', 'issue_updated', 'news_added'], Setting.find_by_name('notified_events').value end - + def test_setting_should_be_reloaded_after_clear_cache Setting.app_title = "My title" assert_equal "My title", Setting.app_title - + s = Setting.find_by_name("app_title") s.value = 'New title' s.save! assert_equal "My title", Setting.app_title - + Setting.clear_cache assert_equal "New title", Setting.app_title end diff -r d98d22a98252 -r fb9a13467253 test/unit/time_entry_activity_test.rb --- a/test/unit/time_entry_activity_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/time_entry_activity_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -84,5 +84,33 @@ e.reload assert_equal "0", e.custom_value_for(field).value end + + def test_system_activity_with_child_in_use_should_be_in_use + project = Project.generate! + system_activity = TimeEntryActivity.create!(:name => 'Activity') + project_activity = TimeEntryActivity.create!(:name => 'Activity', :project => project, :parent_id => system_activity.id) + + TimeEntry.generate!(:project => project, :activity => project_activity) + + assert project_activity.in_use? + assert system_activity.in_use? + end + + def test_destroying_a_system_activity_should_reassign_children_activities + project = Project.generate! + system_activity = TimeEntryActivity.create!(:name => 'Activity') + project_activity = TimeEntryActivity.create!(:name => 'Activity', :project => project, :parent_id => system_activity.id) + + entries = [ + TimeEntry.generate!(:project => project, :activity => system_activity), + TimeEntry.generate!(:project => project, :activity => project_activity) + ] + + assert_difference 'TimeEntryActivity.count', -2 do + assert_nothing_raised do + assert system_activity.destroy(TimeEntryActivity.find_by_name('Development')) + end + end + assert entries.all? {|entry| entry.reload.activity.name == 'Development'} + end end - diff -r d98d22a98252 -r fb9a13467253 test/unit/time_entry_query_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/time_entry_query_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -0,0 +1,40 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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 TimeEntryQueryTest < ActiveSupport::TestCase + fixtures :projects, :users, :enumerations + + def test_activity_filter_should_consider_system_and_project_activities + TimeEntry.delete_all + system = TimeEntryActivity.create!(:name => 'Foo') + override = TimeEntryActivity.create!(:name => 'Foo', :parent_id => system.id, :project_id => 1) + other = TimeEntryActivity.create!(:name => 'Bar') + TimeEntry.generate!(:activity => system, :hours => 1.0) + TimeEntry.generate!(:activity => override, :hours => 2.0) + TimeEntry.generate!(:activity => other, :hours => 4.0) + + query = TimeEntryQuery.new(:name => '_') + query.add_filter('activity_id', '=', [system.id.to_s]) + assert_equal 3.0, query.results_scope.sum(:hours) + + query = TimeEntryQuery.new(:name => '_') + query.add_filter('activity_id', '!', [system.id.to_s]) + assert_equal 4.0, query.results_scope.sum(:hours) + end +end diff -r d98d22a98252 -r fb9a13467253 test/unit/time_entry_test.rb --- a/test/unit/time_entry_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/time_entry_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -25,8 +25,7 @@ :journals, :journal_details, :issue_categories, :enumerations, :groups_users, - :enabled_modules, - :workflows + :enabled_modules def test_hours_format assertions = { "2" => 2.0, @@ -111,6 +110,13 @@ assert_equal 1, te.errors.count end + def test_spent_on_with_2_digits_year_should_not_be_valid + entry = TimeEntry.new(:project => Project.find(1), :user => User.find(1), :activity => TimeEntryActivity.first, :hours => 1) + entry.spent_on = "09-02-04" + assert !entry.valid? + assert_include I18n.translate('activerecord.errors.messages.not_a_date'), entry.errors[:spent_on] + end + def test_set_project_if_nil anon = User.anonymous project = Project.find(1) diff -r d98d22a98252 -r fb9a13467253 test/unit/token_test.rb --- a/test/unit/token_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/token_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -58,4 +58,51 @@ assert_equal 2, Token.destroy_expired end end + + def test_find_active_user_should_return_user + token = Token.create!(:user_id => 1, :action => 'api') + assert_equal User.find(1), Token.find_active_user('api', token.value) + end + + def test_find_active_user_should_return_nil_for_locked_user + token = Token.create!(:user_id => 1, :action => 'api') + User.find(1).lock! + assert_nil Token.find_active_user('api', token.value) + end + + def test_find_user_should_return_user + token = Token.create!(:user_id => 1, :action => 'api') + assert_equal User.find(1), Token.find_user('api', token.value) + end + + def test_find_user_should_return_locked_user + token = Token.create!(:user_id => 1, :action => 'api') + User.find(1).lock! + assert_equal User.find(1), Token.find_user('api', token.value) + end + + def test_find_token_should_return_the_token + token = Token.create!(:user_id => 1, :action => 'api') + assert_equal token, Token.find_token('api', token.value) + end + + def test_find_token_should_return_the_token_with_validity + token = Token.create!(:user_id => 1, :action => 'api', :created_on => 1.hour.ago) + assert_equal token, Token.find_token('api', token.value, 1) + end + + def test_find_token_should_return_nil_with_wrong_action + token = Token.create!(:user_id => 1, :action => 'feeds') + assert_nil Token.find_token('api', token.value) + end + + def test_find_token_should_return_nil_without_user + token = Token.create!(:user_id => 999, :action => 'api') + assert_nil Token.find_token('api', token.value) + end + + def test_find_token_should_return_nil_with_validity_expired + token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago) + assert_nil Token.find_token('api', token.value, 1) + end end diff -r d98d22a98252 -r fb9a13467253 test/unit/user_preference_test.rb --- a/test/unit/user_preference_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/user_preference_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -55,6 +55,11 @@ assert_kind_of Hash, up.others end + def test_others_should_be_blank_after_initialization + pref = User.new.pref + assert_equal({}, pref.others) + end + def test_reading_value_from_nil_others_hash up = UserPreference.new(:user => User.new) up.others = nil diff -r d98d22a98252 -r fb9a13467253 test/unit/user_test.rb --- a/test/unit/user_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/user_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -25,8 +25,7 @@ :issue_categories, :enumerations, :issues, :journals, :journal_details, :groups_users, - :enabled_modules, - :workflows + :enabled_modules def setup @admin = User.find(1) @@ -34,10 +33,15 @@ @dlopper = User.find(3) end + def test_sorted_scope_should_sort_user_by_display_name + assert_equal User.all.map(&:name).map(&:downcase).sort, + User.sorted.map(&:name).map(&:downcase) + end + def test_generate User.generate!(:firstname => 'Testing connection') User.generate!(:firstname => 'Testing connection') - assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'}) + assert_equal 2, User.where(:firstname => 'Testing connection').count end def test_truth @@ -67,6 +71,41 @@ assert user.save end + def test_generate_password_should_respect_minimum_password_length + with_settings :password_min_length => 15 do + user = User.generate!(:generate_password => true) + assert user.password.length >= 15 + end + end + + def test_generate_password_should_not_generate_password_with_less_than_10_characters + with_settings :password_min_length => 4 do + user = User.generate!(:generate_password => true) + assert user.password.length >= 10 + end + end + + def test_generate_password_on_create_should_set_password + user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") + user.login = "newuser" + user.generate_password = true + assert user.save + + password = user.password + assert user.check_password?(password) + end + + def test_generate_password_on_update_should_update_password + user = User.find(2) + hash = user.hashed_password + user.generate_password = true + assert user.save + + password = user.password + assert user.check_password?(password) + assert_not_equal hash, user.reload.hashed_password + end + def test_create user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") @@ -100,7 +139,8 @@ u.login = 'newuser' u.password, u.password_confirmation = "password", "password" assert u.save - u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo") + u = User.new(:firstname => "Similar", :lastname => "User", + :mail => "similaruser@somenet.foo") u.login = 'NewUser' u.password, u.password_confirmation = "password", "password" assert !u.save @@ -143,18 +183,18 @@ end def test_destroy_should_delete_members_and_roles - members = Member.find_all_by_user_id(2) - ms = members.size + members = Member.where(:user_id => 2) + ms = members.count rs = members.collect(&:roles).flatten.size - + assert ms > 0 + assert rs > 0 assert_difference 'Member.count', - ms do assert_difference 'MemberRole.count', - rs do User.find(2).destroy end end - assert_nil User.find_by_id(2) - assert Member.find_all_by_user_id(2).empty? + assert_equal 0, Member.where(:user_id => 2).count end def test_destroy_should_update_attachments @@ -169,7 +209,8 @@ def test_destroy_should_update_comments comment = Comment.create!( - :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'), + :commented => News.create!(:project_id => 1, + :author_id => 1, :title => 'foo', :description => 'foo'), :author => User.find(2), :comments => 'foo' ) @@ -180,7 +221,8 @@ end def test_destroy_should_update_issues - issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo') + issue = Issue.create!(:project_id => 1, :author_id => 2, + :tracker_id => 1, :subject => 'foo') User.find(2).destroy assert_nil User.find_by_id(2) @@ -188,7 +230,8 @@ end def test_destroy_should_unassign_issues - issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2) + issue = Issue.create!(:project_id => 1, :author_id => 1, + :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2) User.find(2).destroy assert_nil User.find_by_id(2) @@ -196,7 +239,8 @@ end def test_destroy_should_update_journals - issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo') + issue = Issue.create!(:project_id => 1, :author_id => 2, + :tracker_id => 1, :subject => 'foo') issue.init_journal(User.find(2), "update") issue.save! @@ -206,13 +250,14 @@ end def test_destroy_should_update_journal_details_old_value - issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2) + issue = Issue.create!(:project_id => 1, :author_id => 1, + :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2) issue.init_journal(User.find(1), "update") issue.assigned_to_id = nil assert_difference 'JournalDetail.count' do issue.save! end - journal_detail = JournalDetail.first(:order => 'id DESC') + journal_detail = JournalDetail.order('id DESC').first assert_equal '2', journal_detail.old_value User.find(2).destroy @@ -221,13 +266,14 @@ end def test_destroy_should_update_journal_details_value - issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo') + issue = Issue.create!(:project_id => 1, :author_id => 1, + :tracker_id => 1, :subject => 'foo') issue.init_journal(User.find(1), "update") issue.assigned_to_id = 2 assert_difference 'JournalDetail.count' do issue.save! end - journal_detail = JournalDetail.first(:order => 'id DESC') + journal_detail = JournalDetail.order('id DESC').first assert_equal '2', journal_detail.value User.find(2).destroy @@ -237,23 +283,23 @@ def test_destroy_should_update_messages board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board') - message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo') - + message = Message.create!(:board_id => board.id, :author_id => 2, + :subject => 'foo', :content => 'foo') User.find(2).destroy assert_nil User.find_by_id(2) assert_equal User.anonymous, message.reload.author end def test_destroy_should_update_news - news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo') - + news = News.create!(:project_id => 1, :author_id => 2, + :title => 'foo', :description => 'foo') User.find(2).destroy assert_nil User.find_by_id(2) assert_equal User.anonymous, news.reload.author end def test_destroy_should_delete_private_queries - query = Query.new(:name => 'foo', :is_public => false) + query = Query.new(:name => 'foo', :visibility => Query::VISIBILITY_PRIVATE) query.project_id = 1 query.user_id = 2 query.save! @@ -264,7 +310,7 @@ end def test_destroy_should_update_public_queries - query = Query.new(:name => 'foo', :is_public => true) + query = Query.new(:name => 'foo', :visibility => Query::VISIBILITY_PUBLIC) query.project_id = 1 query.user_id = 2 query.save! @@ -275,7 +321,8 @@ end def test_destroy_should_update_time_entries - entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo')) + entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, + :activity => TimeEntryActivity.create!(:name => 'foo')) entry.project_id = 1 entry.user_id = 2 entry.save! @@ -294,7 +341,8 @@ end def test_destroy_should_delete_watchers - issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo') + issue = Issue.create!(:project_id => 1, :author_id => 1, + :tracker_id => 1, :subject => 'foo') watcher = Watcher.create!(:user_id => 2, :watchable => issue) User.find(2).destroy @@ -306,7 +354,9 @@ wiki_content = WikiContent.create!( :text => 'foo', :author_id => 2, - :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start')) + :page => WikiPage.create!(:title => 'Foo', + :wiki => Wiki.create!(:project_id => 3, + :start_page => 'Start')) ) wiki_content.text = 'bar' assert_difference 'WikiContent::Version.count' do @@ -363,28 +413,7 @@ u = User.new u.mail_notification = 'foo' u.save - assert_not_nil u.errors[:mail_notification] - end - - context "User#try_to_login" do - should "fall-back to case-insensitive if user login is not found as-typed." do - user = User.try_to_login("AdMin", "admin") - assert_kind_of User, user - assert_equal "admin", user.login - end - - should "select the exact matching user first" do - case_sensitive_user = User.generate! do |user| - user.password = "admin123" - end - # bypass validations to make it appear like existing data - case_sensitive_user.update_attribute(:login, 'ADMIN') - - user = User.try_to_login("ADMIN", "admin123") - assert_kind_of User, user - assert_equal "ADMIN", user.login - - end + assert_not_equal [], u.errors[:mail_notification] end def test_password @@ -401,7 +430,8 @@ def test_validate_password_length with_settings :password_min_length => '100' do - user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo") + user = User.new(:firstname => "new100", + :lastname => "user100", :mail => "newuser100@somenet.foo") user.login = "newuser100" user.password, user.password_confirmation = "password100", "password100" assert !user.save @@ -412,6 +442,11 @@ def test_name_format assert_equal 'John S.', @jsmith.name(:firstname_lastinitial) assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname) + assert_equal 'J. Smith', @jsmith.name(:firstinitial_lastname) + assert_equal 'J.-P. Lang', User.new(:firstname => 'Jean-Philippe', :lastname => 'Lang').name(:firstinitial_lastname) + end + + def test_name_should_use_setting_as_default_format with_settings :user_format => :firstname_lastname do assert_equal 'John Smith', @jsmith.reload.name end @@ -456,53 +491,74 @@ def test_fields_for_order_statement_should_return_fields_according_user_format_setting with_settings :user_format => 'lastname_coma_firstname' do - assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement - end - end - - def test_fields_for_order_statement_width_table_name_should_prepend_table_name - with_settings :user_format => 'lastname_firstname' do - assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors') - end - end - - def test_fields_for_order_statement_with_blank_format_should_return_default - with_settings :user_format => '' do - assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement - end - end - - def test_fields_for_order_statement_with_invalid_format_should_return_default - with_settings :user_format => 'foo' do - assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement + assert_equal ['users.lastname', 'users.firstname', 'users.id'], + User.fields_for_order_statement end end - def test_lock - user = User.try_to_login("jsmith", "jsmith") - assert_equal @jsmith, user + def test_fields_for_order_statement_width_table_name_should_prepend_table_name + with_settings :user_format => 'lastname_firstname' do + assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], + User.fields_for_order_statement('authors') + end + end + def test_fields_for_order_statement_with_blank_format_should_return_default + with_settings :user_format => '' do + assert_equal ['users.firstname', 'users.lastname', 'users.id'], + User.fields_for_order_statement + end + end + + def test_fields_for_order_statement_with_invalid_format_should_return_default + with_settings :user_format => 'foo' do + assert_equal ['users.firstname', 'users.lastname', 'users.id'], + User.fields_for_order_statement + end + end + + test ".try_to_login with good credentials should return the user" do + user = User.try_to_login("admin", "admin") + assert_kind_of User, user + assert_equal "admin", user.login + end + + test ".try_to_login with wrong credentials should return nil" do + assert_nil User.try_to_login("admin", "foo") + end + + def test_try_to_login_with_locked_user_should_return_nil @jsmith.status = User::STATUS_LOCKED - assert @jsmith.save + @jsmith.save! user = User.try_to_login("jsmith", "jsmith") assert_equal nil, user end - context ".try_to_login" do - context "with good credentials" do - should "return the user" do - user = User.try_to_login("admin", "admin") - assert_kind_of User, user - assert_equal "admin", user.login - end + def test_try_to_login_with_locked_user_and_not_active_only_should_return_user + @jsmith.status = User::STATUS_LOCKED + @jsmith.save! + + user = User.try_to_login("jsmith", "jsmith", false) + assert_equal @jsmith, user + end + + test ".try_to_login should fall-back to case-insensitive if user login is not found as-typed" do + user = User.try_to_login("AdMin", "admin") + assert_kind_of User, user + assert_equal "admin", user.login + end + + test ".try_to_login should select the exact matching user first" do + case_sensitive_user = User.generate! do |user| + user.password = "admin123" end + # bypass validations to make it appear like existing data + case_sensitive_user.update_attribute(:login, 'ADMIN') - context "with wrong credentials" do - should "return nil" do - assert_nil User.try_to_login("admin", "foo") - end - end + user = User.try_to_login("ADMIN", "admin123") + assert_kind_of User, user + assert_equal "ADMIN", user.login end if ldap_configured? @@ -580,7 +636,7 @@ @auth_source.account_password = '' @auth_source.save! end - + context "with a successful authentication" do should "create a new user account if it doesn't exist" do assert_difference('User.count') do @@ -589,7 +645,7 @@ end end end - + context "with an unsuccessful authentication" do should "return nil" do assert_nil User.try_to_login('example1', '11111') @@ -646,50 +702,46 @@ end end - context "User#api_key" do - should "generate a new one if the user doesn't have one" do - user = User.generate!(:api_token => nil) - assert_nil user.api_token + test "#api_key should generate a new one if the user doesn't have one" do + user = User.generate!(:api_token => nil) + assert_nil user.api_token - key = user.api_key - assert_equal 40, key.length - user.reload - assert_equal key, user.api_key - end - - should "return the existing api token value" do - user = User.generate! - token = Token.create!(:action => 'api') - user.api_token = token - assert user.save - - assert_equal token.value, user.api_key - end + key = user.api_key + assert_equal 40, key.length + user.reload + assert_equal key, user.api_key end - context "User#find_by_api_key" do - should "return nil if no matching key is found" do - assert_nil User.find_by_api_key('zzzzzzzzz') - end + test "#api_key should return the existing api token value" do + user = User.generate! + token = Token.create!(:action => 'api') + user.api_token = token + assert user.save - should "return nil if the key is found for an inactive user" do - user = User.generate! - user.status = User::STATUS_LOCKED - token = Token.create!(:action => 'api') - user.api_token = token - user.save + assert_equal token.value, user.api_key + end - assert_nil User.find_by_api_key(token.value) - end + test "#find_by_api_key should return nil if no matching key is found" do + assert_nil User.find_by_api_key('zzzzzzzzz') + end - should "return the user if the key is found for an active user" do - user = User.generate! - token = Token.create!(:action => 'api') - user.api_token = token - user.save + test "#find_by_api_key should return nil if the key is found for an inactive user" do + user = User.generate! + user.status = User::STATUS_LOCKED + token = Token.create!(:action => 'api') + user.api_token = token + user.save - assert_equal user, User.find_by_api_key(token.value) - end + assert_nil User.find_by_api_key(token.value) + end + + test "#find_by_api_key should return the user if the key is found for an active user" do + user = User.generate! + token = Token.create!(:action => 'api') + user.api_token = token + user.save + + assert_equal user, User.find_by_api_key(token.value) end def test_default_admin_account_changed_should_return_false_if_account_was_not_changed @@ -724,6 +776,32 @@ assert_equal true, User.default_admin_account_changed? end + def test_membership_with_project_should_return_membership + project = Project.find(1) + + membership = @jsmith.membership(project) + assert_kind_of Member, membership + assert_equal @jsmith, membership.user + assert_equal project, membership.project + end + + def test_membership_with_project_id_should_return_membership + project = Project.find(1) + + membership = @jsmith.membership(1) + assert_kind_of Member, membership + assert_equal @jsmith, membership.user + assert_equal project, membership.project + end + + def test_membership_for_non_member_should_return_nil + project = Project.find(1) + + user = User.generate! + membership = user.membership(1) + assert_nil membership + end + def test_roles_for_project # user with a role roles = @jsmith.roles_for_project(Project.find(1)) @@ -816,29 +894,27 @@ assert !u.password_confirmation.blank? end - context "#change_password_allowed?" do - should "be allowed if no auth source is set" do - user = User.generate! - assert user.change_password_allowed? - end + test "#change_password_allowed? should be allowed if no auth source is set" do + user = User.generate! + assert user.change_password_allowed? + end - should "delegate to the auth source" do - user = User.generate! + test "#change_password_allowed? should delegate to the auth source" do + user = User.generate! - allowed_auth_source = AuthSource.generate! - def allowed_auth_source.allow_password_changes?; true; end + allowed_auth_source = AuthSource.generate! + def allowed_auth_source.allow_password_changes?; true; end - denied_auth_source = AuthSource.generate! - def denied_auth_source.allow_password_changes?; false; end + denied_auth_source = AuthSource.generate! + def denied_auth_source.allow_password_changes?; false; end - assert user.change_password_allowed? + assert user.change_password_allowed? - user.auth_source = allowed_auth_source - assert user.change_password_allowed?, "User not allowed to change password, though auth source does" + user.auth_source = allowed_auth_source + assert user.change_password_allowed?, "User not allowed to change password, though auth source does" - user.auth_source = denied_auth_source - assert !user.change_password_allowed?, "User allowed to change password, though auth source does not" - end + user.auth_source = denied_auth_source + assert !user.change_password_allowed?, "User allowed to change password, though auth source does not" end def test_own_account_deletable_should_be_true_with_unsubscrive_enabled @@ -901,7 +977,7 @@ should "authorize nearly everything for admin users" do project = Project.find(1) assert ! @admin.member_of?(project) - %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p| + %w(edit_issues delete_issues manage_news add_documents manage_wiki).each do |p| assert_equal true, @admin.allowed_to?(p.to_sym, project) end end @@ -1014,9 +1090,15 @@ assert ! @user.notify_about?(@issue) end end + end - context "other events" do - should 'be added and tested' + def test_notify_about_news + user = User.generate! + news = News.new + + User::MAIL_NOTIFICATION_OPTIONS.map(&:first).each do |option| + user.mail_notification = option + assert_equal (option != 'none'), user.notify_about?(news) end end @@ -1038,38 +1120,35 @@ end if Object.const_defined?(:OpenID) + def test_setting_identity_url + normalized_open_id_url = 'http://example.com/' + u = User.new( :identity_url => 'http://example.com/' ) + assert_equal normalized_open_id_url, u.identity_url + end - def test_setting_identity_url - normalized_open_id_url = 'http://example.com/' - u = User.new( :identity_url => 'http://example.com/' ) - assert_equal normalized_open_id_url, u.identity_url - end + def test_setting_identity_url_without_trailing_slash + normalized_open_id_url = 'http://example.com/' + u = User.new( :identity_url => 'http://example.com' ) + assert_equal normalized_open_id_url, u.identity_url + end - def test_setting_identity_url_without_trailing_slash - normalized_open_id_url = 'http://example.com/' - u = User.new( :identity_url => 'http://example.com' ) - assert_equal normalized_open_id_url, u.identity_url - end + def test_setting_identity_url_without_protocol + normalized_open_id_url = 'http://example.com/' + u = User.new( :identity_url => 'example.com' ) + assert_equal normalized_open_id_url, u.identity_url + end - def test_setting_identity_url_without_protocol - normalized_open_id_url = 'http://example.com/' - u = User.new( :identity_url => 'example.com' ) - assert_equal normalized_open_id_url, u.identity_url - end + def test_setting_blank_identity_url + u = User.new( :identity_url => 'example.com' ) + u.identity_url = '' + assert u.identity_url.blank? + end - def test_setting_blank_identity_url - u = User.new( :identity_url => 'example.com' ) - u.identity_url = '' - assert u.identity_url.blank? - end - - def test_setting_invalid_identity_url - u = User.new( :identity_url => 'this is not an openid url' ) - assert u.identity_url.blank? - end - + def test_setting_invalid_identity_url + u = User.new( :identity_url => 'this is not an openid url' ) + assert u.identity_url.blank? + end else puts "Skipping openid tests." end - end diff -r d98d22a98252 -r fb9a13467253 test/unit/version_test.rb --- a/test/unit/version_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/version_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -18,25 +18,27 @@ require File.expand_path('../../test_helper', __FILE__) class VersionTest < ActiveSupport::TestCase - fixtures :projects, :users, :issues, :issue_statuses, :trackers, :enumerations, :versions, :projects_trackers - - def setup - end + fixtures :projects, :users, :issues, :issue_statuses, :trackers, + :enumerations, :versions, :projects_trackers def test_create - v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25') + v = Version.new(:project => Project.find(1), :name => '1.1', + :effective_date => '2011-03-25') assert v.save assert_equal 'open', v.status assert_equal 'none', v.sharing end def test_invalid_effective_date_validation - v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '99999-01-01') + v = Version.new(:project => Project.find(1), :name => '1.1', + :effective_date => '99999-01-01') assert !v.valid? v.effective_date = '2012-11-33' assert !v.valid? v.effective_date = '2012-31-11' assert !v.valid? + v.effective_date = '-2012-31-11' + assert !v.valid? v.effective_date = 'ABC' assert !v.valid? assert_include I18n.translate('activerecord.errors.messages.not_a_date'), @@ -46,8 +48,8 @@ def test_progress_should_be_0_with_no_assigned_issues project = Project.find(1) v = Version.create!(:project => project, :name => 'Progress') - assert_equal 0, v.completed_pourcent - assert_equal 0, v.closed_pourcent + assert_equal 0, v.completed_percent + assert_equal 0, v.closed_percent end def test_progress_should_be_0_with_unbegun_assigned_issues @@ -55,20 +57,20 @@ v = Version.create!(:project => project, :name => 'Progress') add_issue(v) add_issue(v, :done_ratio => 0) - assert_progress_equal 0, v.completed_pourcent - assert_progress_equal 0, v.closed_pourcent + assert_progress_equal 0, v.completed_percent + assert_progress_equal 0, v.closed_percent end def test_progress_should_be_100_with_closed_assigned_issues project = Project.find(1) - status = IssueStatus.find(:first, :conditions => {:is_closed => true}) + status = IssueStatus.where(:is_closed => true).first v = Version.create!(:project => project, :name => 'Progress') add_issue(v, :status => status) add_issue(v, :status => status, :done_ratio => 20) add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25) add_issue(v, :status => status, :estimated_hours => 15) - assert_progress_equal 100.0, v.completed_pourcent - assert_progress_equal 100.0, v.closed_pourcent + assert_progress_equal 100.0, v.completed_percent + assert_progress_equal 100.0, v.closed_percent end def test_progress_should_consider_done_ratio_of_open_assigned_issues @@ -77,8 +79,8 @@ add_issue(v) add_issue(v, :done_ratio => 20) add_issue(v, :done_ratio => 70) - assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_pourcent - assert_progress_equal 0, v.closed_pourcent + assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_percent + assert_progress_equal 0, v.closed_percent end def test_progress_should_consider_closed_issues_as_completed @@ -86,9 +88,9 @@ v = Version.create!(:project => project, :name => 'Progress') add_issue(v) add_issue(v, :done_ratio => 20) - add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})) - assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_pourcent - assert_progress_equal (100.0)/3, v.closed_pourcent + add_issue(v, :status => IssueStatus.where(:is_closed => true).first) + assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_percent + assert_progress_equal (100.0)/3, v.closed_percent end def test_progress_should_consider_estimated_hours_to_weigth_issues @@ -97,20 +99,20 @@ add_issue(v, :estimated_hours => 10) add_issue(v, :estimated_hours => 20, :done_ratio => 30) add_issue(v, :estimated_hours => 40, :done_ratio => 10) - add_issue(v, :estimated_hours => 25, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})) - assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_pourcent - assert_progress_equal 25.0/95.0*100, v.closed_pourcent + add_issue(v, :estimated_hours => 25, :status => IssueStatus.where(:is_closed => true).first) + assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_percent + assert_progress_equal 25.0/95.0*100, v.closed_percent end def test_progress_should_consider_average_estimated_hours_to_weigth_unestimated_issues project = Project.find(1) v = Version.create!(:project => project, :name => 'Progress') add_issue(v, :done_ratio => 20) - add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})) + add_issue(v, :status => IssueStatus.where(:is_closed => true).first) add_issue(v, :estimated_hours => 10, :done_ratio => 30) add_issue(v, :estimated_hours => 40, :done_ratio => 10) - assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent - assert_progress_equal 25.0/100.0*100, v.closed_pourcent + assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_percent + assert_progress_equal 25.0/100.0*100, v.closed_percent end def test_should_sort_scheduled_then_unscheduled_versions @@ -130,75 +132,64 @@ assert_equal false, version.completed? end - context "#behind_schedule?" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.create!(:name => 'test0', :identifier => 'test0') - @project.trackers << Tracker.create!(:name => 'track') - - @version = Version.create!(:project => @project, :effective_date => nil, :name => 'version') - end - - should "be false if there are no issues assigned" do - @version.update_attribute(:effective_date, Date.yesterday) - assert_equal false, @version.behind_schedule? - end - - should "be false if there is no effective_date" do - assert_equal false, @version.behind_schedule? - end - - should "be false if all of the issues are ahead of schedule" do - @version.update_attribute(:effective_date, 7.days.from_now.to_date) - add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left - add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left - assert_equal 60, @version.completed_pourcent - assert_equal false, @version.behind_schedule? - end - - should "be true if any of the issues are behind schedule" do - @version.update_attribute(:effective_date, 7.days.from_now.to_date) - add_issue(@version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left - add_issue(@version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left - assert_equal 40, @version.completed_pourcent - assert_equal true, @version.behind_schedule? - end - - should "be false if all of the issues are complete" do - @version.update_attribute(:effective_date, 7.days.from_now.to_date) - add_issue(@version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span - add_issue(@version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span - assert_equal 100, @version.completed_pourcent - assert_equal false, @version.behind_schedule? - end + test "#behind_schedule? should be false if there are no issues assigned" do + version = Version.generate!(:effective_date => Date.yesterday) + assert_equal false, version.behind_schedule? end - context "#estimated_hours" do - setup do - @version = Version.create!(:project_id => 1, :name => '#estimated_hours') - end + test "#behind_schedule? should be false if there is no effective_date" do + version = Version.generate!(:effective_date => nil) + assert_equal false, version.behind_schedule? + end - should "return 0 with no assigned issues" do - assert_equal 0, @version.estimated_hours - end + test "#behind_schedule? should be false if all of the issues are ahead of schedule" do + version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date) + add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left + add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left + assert_equal 60, version.completed_percent + assert_equal false, version.behind_schedule? + end - should "return 0 with no estimated hours" do - add_issue(@version) - assert_equal 0, @version.estimated_hours - end + test "#behind_schedule? should be true if any of the issues are behind schedule" do + version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date) + add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left + add_issue(version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left + assert_equal 40, version.completed_percent + assert_equal true, version.behind_schedule? + end - should "return the sum of estimated hours" do - add_issue(@version, :estimated_hours => 2.5) - add_issue(@version, :estimated_hours => 5) - assert_equal 7.5, @version.estimated_hours - end + test "#behind_schedule? should be false if all of the issues are complete" do + version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date) + add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span + add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span + assert_equal 100, version.completed_percent + assert_equal false, version.behind_schedule? + end - should "return the sum of leaves estimated hours" do - parent = add_issue(@version) - add_issue(@version, :estimated_hours => 2.5, :parent_issue_id => parent.id) - add_issue(@version, :estimated_hours => 5, :parent_issue_id => parent.id) - assert_equal 7.5, @version.estimated_hours - end + test "#estimated_hours should return 0 with no assigned issues" do + version = Version.generate! + assert_equal 0, version.estimated_hours + end + + test "#estimated_hours should return 0 with no estimated hours" do + version = Version.create!(:project_id => 1, :name => 'test') + add_issue(version) + assert_equal 0, version.estimated_hours + end + + test "#estimated_hours should return return the sum of estimated hours" do + version = Version.create!(:project_id => 1, :name => 'test') + add_issue(version, :estimated_hours => 2.5) + add_issue(version, :estimated_hours => 5) + assert_equal 7.5, version.estimated_hours + end + + test "#estimated_hours should return the sum of leaves estimated hours" do + version = Version.create!(:project_id => 1, :name => 'test') + parent = add_issue(version) + add_issue(version, :estimated_hours => 2.5, :parent_issue_id => parent.id) + add_issue(version, :estimated_hours => 5, :parent_issue_id => parent.id) + assert_equal 7.5, version.estimated_hours end test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do @@ -225,11 +216,13 @@ # Project 1 now out of the shared scope project_1_issue.reload - assert_equal nil, project_1_issue.fixed_version, "Fixed version is still set after changing the Version's sharing" + assert_equal nil, project_1_issue.fixed_version, + "Fixed version is still set after changing the Version's sharing" # Project 5 now out of the shared scope project_5_issue.reload - assert_equal nil, project_5_issue.fixed_version, "Fixed version is still set after changing the Version's sharing" + assert_equal nil, project_5_issue.fixed_version, + "Fixed version is still set after changing the Version's sharing" # Project 2 issue remains project_2_issue.reload @@ -242,8 +235,8 @@ Issue.create!({:project => version.project, :fixed_version => version, :subject => 'Test', - :author => User.find(:first), - :tracker => version.project.trackers.find(:first)}.merge(attributes)) + :author => User.first, + :tracker => version.project.trackers.first}.merge(attributes)) end def assert_progress_equal(expected_float, actual_float, message="") diff -r d98d22a98252 -r fb9a13467253 test/unit/watcher_test.rb --- a/test/unit/watcher_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/watcher_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -56,12 +56,12 @@ def test_watcher_users watcher_users = Issue.find(2).watcher_users - assert_kind_of Array, watcher_users + assert_kind_of Array, watcher_users.collect{|w| w} assert_kind_of User, watcher_users.first end def test_watcher_users_should_not_validate_user - User.update_all("firstname = ''", "id=1") + User.where(:id => 1).update_all("firstname = ''") @user.reload assert !@user.valid? @@ -99,6 +99,23 @@ assert_nil issue.addable_watcher_users.detect {|user| !issue.visible?(user)} end + def test_any_watched_should_return_false_if_no_object_is_watched + objects = (0..2).map {Issue.generate!} + + assert_equal false, Watcher.any_watched?(objects, @user) + end + + def test_any_watched_should_return_true_if_one_object_is_watched + objects = (0..2).map {Issue.generate!} + objects.last.add_watcher(@user) + + assert_equal true, Watcher.any_watched?(objects, @user) + end + + def test_any_watched_should_return_false_with_no_object + assert_equal false, Watcher.any_watched?([], @user) + end + def test_recipients @issue.watchers.delete_all @issue.reload diff -r d98d22a98252 -r fb9a13467253 test/unit/wiki_content_test.rb --- a/test/unit/wiki_content_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/wiki_content_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -48,11 +48,12 @@ page = WikiPage.new(:wiki => @wiki, :title => "A new page") page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comments => "My comment") - with_settings :notified_events => %w(wiki_content_added) do + with_settings :default_language => 'en', :notified_events => %w(wiki_content_added) do assert page.save end assert_equal 1, ActionMailer::Base.deliveries.size + assert_include 'wiki page has been added', mail_body(ActionMailer::Base.deliveries.last) end def test_update_should_be_versioned @@ -66,7 +67,7 @@ assert_equal version_count+1, content.version assert_equal version_count+1, content.versions.length - version = WikiContent::Version.first(:order => 'id DESC') + version = WikiContent::Version.order('id DESC').first assert_equal @page.id, version.page_id assert_equal '', version.compression assert_equal "My new content", version.data @@ -82,7 +83,7 @@ end end - version = WikiContent::Version.first(:order => 'id DESC') + version = WikiContent::Version.order('id DESC').first assert_equal @page.id, version.page_id assert_equal 'gzip', version.compression assert_not_equal "My new content", version.data @@ -99,6 +100,7 @@ end assert_equal 1, ActionMailer::Base.deliveries.size + assert_include 'wiki page has been updated', mail_body(ActionMailer::Base.deliveries.last) end def test_fetch_history @@ -115,12 +117,12 @@ page.reload assert_equal 500.kilobyte, page.content.text.size end - + def test_current_version content = WikiContent.find(11) assert_equal true, content.current_version? - assert_equal true, content.versions.first(:order => 'version DESC').current_version? - assert_equal false, content.versions.first(:order => 'version ASC').current_version? + assert_equal true, content.versions.order('version DESC').first.current_version? + assert_equal false, content.versions.order('version ASC').first.current_version? end def test_previous_for_first_version_should_return_nil diff -r d98d22a98252 -r fb9a13467253 test/unit/wiki_content_version_test.rb --- a/test/unit/wiki_content_version_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/wiki_content_version_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 diff -r d98d22a98252 -r fb9a13467253 test/unit/wiki_page_test.rb --- a/test/unit/wiki_page_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/wiki_page_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -106,8 +106,8 @@ page.destroy assert_nil WikiPage.find_by_id(1) # make sure that page content and its history are deleted - assert WikiContent.find_all_by_page_id(1).empty? - assert WikiContent.versioned_class.find_all_by_page_id(1).empty? + assert_equal 0, WikiContent.where(:page_id => 1).count + assert_equal 0, WikiContent.versioned_class.where(:page_id => 1).count end def test_destroy_should_not_nullify_children @@ -117,15 +117,15 @@ page.destroy assert_nil WikiPage.find_by_id(2) - children = WikiPage.find_all_by_id(child_ids) - assert_equal child_ids.size, children.size + children = WikiPage.where(:id => child_ids) + assert_equal child_ids.size, children.count children.each do |child| assert_nil child.parent_id end end def test_updated_on_eager_load - page = WikiPage.with_updated_on.first(:order => 'id') + page = WikiPage.with_updated_on.order('id').first assert page.is_a?(WikiPage) assert_not_nil page.read_attribute(:updated_on) assert_equal Time.gm(2007, 3, 6, 23, 10, 51), page.content.updated_on diff -r d98d22a98252 -r fb9a13467253 test/unit/wiki_redirect_test.rb --- a/test/unit/wiki_redirect_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/wiki_redirect_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -69,6 +69,6 @@ assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') @original.destroy - assert !@wiki.redirects.find(:first) + assert_nil @wiki.redirects.first end end diff -r d98d22a98252 -r fb9a13467253 test/unit/wiki_test.rb --- a/test/unit/wiki_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/wiki_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -23,7 +23,7 @@ fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions def test_create - wiki = Wiki.new(:project => Project.find(2)) + wiki = Wiki.new(:project => Project.find(3)) assert !wiki.save assert_equal 1, wiki.errors.count @@ -84,22 +84,18 @@ assert_equal ja_test, Wiki.titleize(ja_test) end - context "#sidebar" do - setup do - @wiki = Wiki.find(1) - end + def test_sidebar_should_return_nil_if_undefined + @wiki = Wiki.find(1) + assert_nil @wiki.sidebar + end - should "return nil if undefined" do - assert_nil @wiki.sidebar - end + def test_sidebar_should_return_a_wiki_page_if_defined + @wiki = Wiki.find(1) + page = @wiki.pages.new(:title => 'Sidebar') + page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar') + page.save! - should "return a WikiPage if defined" do - page = @wiki.pages.new(:title => 'Sidebar') - page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar') - page.save! - - assert_kind_of WikiPage, @wiki.sidebar - assert_equal 'Sidebar', @wiki.sidebar.title - end + assert_kind_of WikiPage, @wiki.sidebar + assert_equal 'Sidebar', @wiki.sidebar.title end end diff -r d98d22a98252 -r fb9a13467253 test/unit/workflow_test.rb --- a/test/unit/workflow_test.rb Wed May 07 14:15:02 2014 +0100 +++ b/test/unit/workflow_test.rb Thu Sep 11 12:45:02 2014 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2014 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 @@ -30,9 +30,9 @@ WorkflowTransition.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2)) end - assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false}) - assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true}) - assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false}) + assert WorkflowTransition.where(:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false).first + assert WorkflowTransition.where(:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true).first + assert WorkflowTransition.where(:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false).first end def test_workflow_permission_should_validate_rule