# HG changeset patch # User Chris Cannam # Date 1389710262 0 # Node ID 261b3d9a4903c28d2b582f13ef5035ad20ba9566 # Parent 038ba2d95de864d8388e537f66ed1929fdf49c93 Update to Redmine 2.4 branch rev 12663 diff -r 038ba2d95de8 -r 261b3d9a4903 .gitignore --- a/.gitignore Fri Jun 14 09:05:06 2013 +0100 +++ b/.gitignore Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,7 @@ /.project /.loadpath +/.powrc +/.rvmrc /config/additional_environment.rb /config/configuration.yml /config/database.yml @@ -15,8 +17,14 @@ /lib/redmine/scm/adapters/mercurial/redminehelper.pyo /log/*.log* /log/mongrel_debug +/plugins/* +!/plugins/README /public/dispatch.* /public/plugin_assets +/public/themes/* +!/public/themes/alternate +!/public/themes/classic +!/public/themes/README /tmp/* /tmp/cache/* /tmp/pdf/* diff -r 038ba2d95de8 -r 261b3d9a4903 .hgignore --- a/.hgignore Fri Jun 14 09:05:06 2013 +0100 +++ b/.hgignore Tue Jan 14 14:37:42 2014 +0000 @@ -2,6 +2,8 @@ .project .loadpath +.powrc +.rvmrc config/additional_environment.rb config/configuration.yml config/database.yml diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/entries --- a/.svn/entries Fri Jun 14 09:05:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -12 diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/format --- a/.svn/format Fri Jun 14 09:05:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -12 diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/0009a621965fc195e93ba67a7d3500cbcd38b084.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/0009a621965fc195e93ba67a7d3500cbcd38b084.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,94 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class DocumentsController < ApplicationController + default_search_scope :documents + model_object Document + before_filter :find_project_by_project_id, :only => [:index, :new, :create] + before_filter :find_model_object, :except => [:index, :new, :create] + before_filter :find_project_from_association, :except => [:index, :new, :create] + before_filter :authorize + + helper :attachments + + def index + @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category' + documents = @project.documents.includes(:attachments, :category).all + case @sort_by + when 'date' + @grouped = documents.group_by {|d| d.updated_on.to_date } + when 'title' + @grouped = documents.group_by {|d| d.title.first.upcase} + when 'author' + @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author} + else + @grouped = documents.group_by(&:category) + end + @document = @project.documents.build + render :layout => false if request.xhr? + end + + def show + @attachments = @document.attachments.all + end + + def new + @document = @project.documents.build + @document.safe_attributes = params[:document] + end + + def create + @document = @project.documents.build + @document.safe_attributes = params[:document] + @document.save_attachments(params[:attachments]) + if @document.save + render_attachment_warning_if_needed(@document) + flash[:notice] = l(:notice_successful_create) + redirect_to project_documents_path(@project) + else + render :action => 'new' + end + end + + def edit + end + + def update + @document.safe_attributes = params[:document] + if request.put? and @document.save + flash[:notice] = l(:notice_successful_update) + redirect_to document_path(@document) + else + render :action => 'edit' + end + end + + def destroy + @document.destroy if request.delete? + redirect_to project_documents_path(@project) + end + + def add_attachment + attachments = Attachment.attach_files(@document, params[:attachments]) + render_attachment_warning_if_needed(@document) + + if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added') + Mailer.attachments_added(attachments[:files]).deliver + end + redirect_to document_path(@document) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00205ce9aaa0cc98a4089319a62880b29732ab31.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00205ce9aaa0cc98a4089319a62880b29732ab31.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,4349 @@ +#============================================================+ +# File name : tcpdf.rb +# Begin : 2002-08-03 +# Last Update : 2007-03-20 +# Author : Nicola Asuni +# Version : 1.53.0.TC031 +# License : GNU LGPL (http://www.gnu.org/copyleft/lesser.html) +# +# Description : This is a Ruby class for generating PDF files +# on-the-fly without requiring external +# extensions. +# +# IMPORTANT: +# This class is an extension and improvement of the Public Domain +# FPDF class by Olivier Plathey (http://www.fpdf.org). +# +# Main changes by Nicola Asuni: +# Ruby porting; +# UTF-8 Unicode support; +# code refactoring; +# source code clean up; +# code style and formatting; +# source code documentation using phpDocumentor (www.phpdoc.org); +# All ISO page formats were included; +# image scale factor; +# includes methods to parse and printsome XHTML code, supporting the following elements: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small; +# includes a method to print various barcode formats using an improved version of "Generic Barcode Render Class" by Karim Mribti (http://www.mribti.com/barcode/) (require GD library: http://www.boutell.com/gd/); +# defines standard Header() and Footer() methods. +# +# Ported to Ruby by Ed Moss 2007-08-06 +# +#============================================================+ + +require 'tempfile' +require 'core/rmagick' + +# +# TCPDF Class. +# @package com.tecnick.tcpdf +# + +PDF_PRODUCER = 'TCPDF via RFPDF 1.53.0.TC031 (http://tcpdf.sourceforge.net)' + +module TCPDFFontDescriptor + @@descriptors = { 'freesans' => {} } + @@font_name = 'freesans' + + def self.font(font_name) + @@descriptors[font_name.gsub(".rb", "")] + end + + def self.define(font_name = 'freesans') + @@descriptors[font_name] ||= {} + yield @@descriptors[font_name] + end +end + +# This is a Ruby class for generating PDF files on-the-fly without requiring external extensions.
+# This class is an extension and improvement of the FPDF class by Olivier Plathey (http://www.fpdf.org).
+# This version contains some changes: [porting to Ruby, support for UTF-8 Unicode, code style and formatting, php documentation (www.phpdoc.org), ISO page formats, minor improvements, image scale factor]
+# TCPDF project (http://tcpdf.sourceforge.net) is based on the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org).
+# To add your own TTF fonts please read /fonts/README.TXT +# @name TCPDF +# @package com.tecnick.tcpdf +# @@version 1.53.0.TC031 +# @author Nicola Asuni +# @link http://tcpdf.sourceforge.net +# @license http://www.gnu.org/copyleft/lesser.html LGPL +# +class TCPDF + include RFPDF + include Core::RFPDF + include RFPDF::Math + + def logger + Rails.logger + end + + @@version = "1.53.0.TC031" + @@fpdf_charwidths = {} + + cattr_accessor :k_cell_height_ratio + @@k_cell_height_ratio = 1.25 + + cattr_accessor :k_blank_image + @@k_blank_image = "" + + cattr_accessor :k_small_ratio + @@k_small_ratio = 2/3.0 + + cattr_accessor :k_path_cache + @@k_path_cache = Rails.root.join('tmp') + + cattr_accessor :k_path_url_cache + @@k_path_url_cache = Rails.root.join('tmp') + + attr_accessor :barcode + + attr_accessor :buffer + + attr_accessor :diffs + + attr_accessor :color_flag + + attr_accessor :default_table_columns + + attr_accessor :max_table_columns + + attr_accessor :default_font + + attr_accessor :draw_color + + attr_accessor :encoding + + attr_accessor :fill_color + + attr_accessor :fonts + + attr_accessor :font_family + + attr_accessor :font_files + + cattr_accessor :font_path + + attr_accessor :font_style + + attr_accessor :font_size_pt + + attr_accessor :header_width + + attr_accessor :header_logo + + attr_accessor :header_logo_width + + attr_accessor :header_title + + attr_accessor :header_string + + attr_accessor :images + + attr_accessor :img_scale + + attr_accessor :in_footer + + attr_accessor :is_unicode + + attr_accessor :lasth + + attr_accessor :links + + attr_accessor :list_ordered + + attr_accessor :list_count + + attr_accessor :li_spacer + + attr_accessor :n + + attr_accessor :offsets + + attr_accessor :orientation_changes + + attr_accessor :page + + attr_accessor :page_links + + attr_accessor :pages + + attr_accessor :pdf_version + + attr_accessor :prevfill_color + + attr_accessor :prevtext_color + + attr_accessor :print_header + + attr_accessor :print_footer + + attr_accessor :state + + attr_accessor :tableborder + + attr_accessor :tdbegin + + attr_accessor :tdwidth + + attr_accessor :tdheight + + attr_accessor :tdalign + + attr_accessor :tdfill + + attr_accessor :tempfontsize + + attr_accessor :text_color + + attr_accessor :underline + + attr_accessor :ws + + # + # This is the class constructor. + # It allows to set up the page format, the orientation and + # the measure unit used in all the methods (except for the font sizes). + # @since 1.0 + # @param string :orientation page orientation. Possible values are (case insensitive): + # @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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/002350bfb6f2834394af5b5afbf87a58fa102afc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/002350bfb6f2834394af5b5afbf87a58fa102afc.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/002a1096c30994f0a75d2638aabe3bf4e1d79ef4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/002a1096c30994f0a75d2638aabe3bf4e1d79ef4.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/006e32915b2ea0756e649e1fb61801b416e2d647.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/006e32915b2ea0756e649e1fb61801b416e2d647.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,88 @@ +# ActsAsWatchable +module Redmine + module Acts + module Watchable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_watchable(options = {}) + return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods) + class_eval do + has_many :watchers, :as => :watchable, :dependent => :delete_all + has_many :watcher_users, :through => :watchers, :source => :user, :validate => false + + scope :watched_by, lambda { |user_id| + joins(:watchers). + where("#{Watcher.table_name}.user_id = ?", user_id) + } + attr_protected :watcher_ids, :watcher_user_ids + end + send :include, Redmine::Acts::Watchable::InstanceMethods + alias_method_chain :watcher_user_ids=, :uniq_ids + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + # Returns an array of users that are proposed as watchers + def addable_watcher_users + users = self.project.users.sort - self.watcher_users + if respond_to?(:visible?) + users.reject! {|user| !visible?(user)} + end + users + end + + # Adds user as a watcher + def add_watcher(user) + self.watchers << Watcher.new(:user => user) + end + + # Removes user from the watchers list + def remove_watcher(user) + return nil unless user && user.is_a?(User) + watchers.where(:user_id => user.id).delete_all + end + + # Adds/removes watcher + def set_watcher(user, watching=true) + watching ? add_watcher(user) : remove_watcher(user) + end + + # Overrides watcher_user_ids= to make user_ids uniq + def watcher_user_ids_with_uniq_ids=(user_ids) + if user_ids.is_a?(Array) + user_ids = user_ids.uniq + end + send :watcher_user_ids_without_uniq_ids=, user_ids + end + + # Returns true if object is watched by +user+ + def watched_by?(user) + !!(user && self.watcher_user_ids.detect {|uid| uid == user.id }) + end + + def notified_watchers + notified = watcher_users.active + notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'} + if respond_to?(:visible?) + notified.reject! {|user| !visible?(user)} + end + notified + end + + # Returns an array of watchers' email addresses + def watcher_recipients + notified_watchers.collect(&:mail) + end + + module ClassMethods; end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/0081b5051789911922bdd0b18a28f0ad87c64e8d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/0081b5051789911922bdd0b18a28f0ad87c64e8d.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,39 @@ +
+<%= link_to l(:label_tracker_new), new_tracker_path, :class => 'icon icon-add' %> +<%= link_to l(:field_summary), fields_trackers_path, :class => 'icon icon-summary' %> +
+ +

<%=l(:label_tracker_plural)%>

+ + + + + + + + + +<% for tracker in @trackers %> + "> + + + + + +<% end %> + +
<%=l(:label_tracker)%><%=l(:button_sort)%>
<%= link_to h(tracker.name), edit_tracker_path(tracker) %> + <% unless tracker.workflow_rules.count > 0 %> + + <%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), workflows_edit_path(:tracker_id => tracker) %>) + + <% end %> + + <%= reorder_links('tracker', {:action => 'update', :id => tracker}, :put) %> + + <%= delete_link tracker_path(tracker) %> +
+ +

<%= pagination_links_full @tracker_pages %>

+ +<% html_title(l(:label_tracker_plural)) -%> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00bd76ee7194e922fe9340f2d39d910c9f79b20e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00bd76ee7194e922fe9340f2d39d910c9f79b20e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,162 @@ +module ObjectHelpers + def User.generate!(attributes={}) + @generated_user_login ||= 'user0' + @generated_user_login.succ! + user = User.new(attributes) + user.login = @generated_user_login.dup if user.login.blank? + user.mail = "#{@generated_user_login}@example.com" if user.mail.blank? + user.firstname = "Bob" if user.firstname.blank? + user.lastname = "Doe" if user.lastname.blank? + yield user if block_given? + user.save! + user + end + + def User.add_to_project(user, project, roles=nil) + roles = Role.find(1) if roles.nil? + roles = [roles] unless roles.is_a?(Array) + Member.create!(:principal => user, :project => project, :roles => roles) + end + + def Group.generate!(attributes={}) + @generated_group_name ||= 'Group 0' + @generated_group_name.succ! + group = Group.new(attributes) + group.name = @generated_group_name.dup if group.name.blank? + yield group if block_given? + group.save! + group + end + + def Project.generate!(attributes={}) + @generated_project_identifier ||= 'project-0000' + @generated_project_identifier.succ! + project = Project.new(attributes) + project.name = @generated_project_identifier.dup if project.name.blank? + project.identifier = @generated_project_identifier.dup if project.identifier.blank? + yield project if block_given? + project.save! + project + end + + def Project.generate_with_parent!(parent, attributes={}) + project = Project.generate!(attributes) + project.set_parent!(parent) + project + end + + def Tracker.generate!(attributes={}) + @generated_tracker_name ||= 'Tracker 0' + @generated_tracker_name.succ! + tracker = Tracker.new(attributes) + tracker.name = @generated_tracker_name.dup if tracker.name.blank? + yield tracker if block_given? + tracker.save! + tracker + end + + def Role.generate!(attributes={}) + @generated_role_name ||= 'Role 0' + @generated_role_name.succ! + role = Role.new(attributes) + role.name = @generated_role_name.dup if role.name.blank? + yield role if block_given? + role.save! + role + end + + def Issue.generate!(attributes={}) + issue = Issue.new(attributes) + issue.project ||= Project.find(1) + issue.tracker ||= issue.project.trackers.first + issue.subject = 'Generated' if issue.subject.blank? + issue.author ||= User.find(2) + yield issue if block_given? + issue.save! + issue + end + + # Generates an issue with 2 children and a grandchild + def Issue.generate_with_descendants!(attributes={}) + issue = Issue.generate!(attributes) + child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id) + Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id) + Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id) + issue.reload + end + + def Journal.generate!(attributes={}) + journal = Journal.new(attributes) + journal.user ||= User.first + journal.journalized ||= Issue.first + yield journal if block_given? + journal.save! + journal + end + + def Version.generate!(attributes={}) + @generated_version_name ||= 'Version 0' + @generated_version_name.succ! + version = Version.new(attributes) + version.name = @generated_version_name.dup if version.name.blank? + yield version if block_given? + version.save! + version + end + + def TimeEntry.generate!(attributes={}) + entry = TimeEntry.new(attributes) + entry.user ||= User.find(2) + entry.issue ||= Issue.find(1) unless entry.project + entry.project ||= entry.issue.project + entry.activity ||= TimeEntryActivity.first + entry.spent_on ||= Date.today + entry.hours ||= 1.0 + entry.save! + entry + end + + def AuthSource.generate!(attributes={}) + @generated_auth_source_name ||= 'Auth 0' + @generated_auth_source_name.succ! + source = AuthSource.new(attributes) + source.name = @generated_auth_source_name.dup if source.name.blank? + yield source if block_given? + source.save! + source + end + + def Board.generate!(attributes={}) + @generated_board_name ||= 'Forum 0' + @generated_board_name.succ! + board = Board.new(attributes) + board.name = @generated_board_name.dup if board.name.blank? + board.description = @generated_board_name.dup if board.description.blank? + yield board if block_given? + board.save! + board + end + + def Attachment.generate!(attributes={}) + @generated_filename ||= 'testfile0' + @generated_filename.succ! + attributes = attributes.dup + attachment = Attachment.new(attributes) + attachment.container ||= Issue.find(1) + attachment.author ||= User.find(2) + attachment.filename = @generated_filename.dup if attachment.filename.blank? + attachment.save! + attachment + end + + def CustomField.generate!(attributes={}) + @generated_custom_field_name ||= 'Custom field 0' + @generated_custom_field_name.succ! + field = new(attributes) + field.name = @generated_custom_field_name.dup if field.name.blank? + field.field_format = 'string' if field.field_format.blank? + yield field if block_given? + field.save! + field + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00c1a9b51dc321ecb980f25b62c13dfa7fd628ff.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00c1a9b51dc321ecb980f25b62c13dfa7fd628ff.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,24 @@ +<%= error_messages_for 'auth_source' %> + +
+

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

+

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

+

<%= f.text_field :port, :required => true, :size => 6 %> <%= f.check_box :tls, :no_label => true %> LDAPS

+

<%= f.text_field :account %>

+

<%= f.password_field :account_password, :label => :field_password, + :name => 'dummy_password', + :value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='auth_source[account_password]';", + :onchange => "this.name='auth_source[account_password]';" %>

+

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

+

<%= f.text_field :filter, :size => 60, :label => :field_auth_source_ldap_filter %>

+

<%= f.text_field :timeout, :size => 4 %>

+

<%= f.check_box :onthefly_register, :label => :field_onthefly %>

+
+ +
<%=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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/00/00e6c0514da25c76abd6063e97c17125ab6f9899.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00e6c0514da25c76abd6063e97c17125ab6f9899.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/01295ecfb41026f1bdc485aae7fd6a8c5187ac83.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01295ecfb41026f1bdc485aae7fd6a8c5187ac83.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/0154cdd03545376764b58ded20a14138238e65d0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/0154cdd03545376764b58ded20a14138238e65d0.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,178 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::Hook::ManagerTest < ActionView::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :groups_users, + :trackers, :projects_trackers, + :enabled_modules, + :versions, + :issue_statuses, :issue_categories, :issue_relations, + :enumerations, + :issues + + # Some hooks that are manually registered in these tests + class TestHook < Redmine::Hook::ViewListener; end + + class TestHook1 < TestHook + def view_layouts_base_html_head(context) + 'Test hook 1 listener.' + end + end + + class TestHook2 < TestHook + def view_layouts_base_html_head(context) + 'Test hook 2 listener.' + end + end + + class TestHook3 < TestHook + def view_layouts_base_html_head(context) + "Context keys: #{context.keys.collect(&:to_s).sort.join(', ')}." + end + end + + class TestLinkToHook < TestHook + def view_layouts_base_html_head(context) + link_to('Issues', :controller => 'issues') + end + end + + class TestHookHelperController < ActionController::Base + include Redmine::Hook::Helper + end + + class TestHookHelperView < ActionView::Base + include Redmine::Hook::Helper + end + + Redmine::Hook.clear_listeners + + def setup + @hook_module = Redmine::Hook + end + + def teardown + @hook_module.clear_listeners + end + + def test_clear_listeners + assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size + @hook_module.add_listener(TestHook1) + @hook_module.add_listener(TestHook2) + assert_equal 2, @hook_module.hook_listeners(:view_layouts_base_html_head).size + + @hook_module.clear_listeners + assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size + end + + def test_add_listener + assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size + @hook_module.add_listener(TestHook1) + assert_equal 1, @hook_module.hook_listeners(:view_layouts_base_html_head).size + end + + def test_call_hook + @hook_module.add_listener(TestHook1) + assert_equal ['Test hook 1 listener.'], hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_with_context + @hook_module.add_listener(TestHook3) + assert_equal ['Context keys: bar, controller, foo, hook_caller, project, request.'], + hook_helper.call_hook(:view_layouts_base_html_head, :foo => 1, :bar => 'a') + end + + def test_call_hook_with_multiple_listeners + @hook_module.add_listener(TestHook1) + @hook_module.add_listener(TestHook2) + assert_equal ['Test hook 1 listener.', 'Test hook 2 listener.'], hook_helper.call_hook(:view_layouts_base_html_head) + end + + # Context: Redmine::Hook::Helper.call_hook default_url + def test_call_hook_default_url_options + @hook_module.add_listener(TestLinkToHook) + + assert_equal ['Issues'], hook_helper.call_hook(:view_layouts_base_html_head) + end + + # Context: Redmine::Hook::Helper.call_hook + def test_call_hook_with_project_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /project/i, hook_helper.call_hook(:view_layouts_base_html_head)[0] + end + + def test_call_hook_from_controller_with_controller_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /controller/i, hook_helper.call_hook(:view_layouts_base_html_head)[0] + end + + def test_call_hook_from_controller_with_request_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /request/i, hook_helper.call_hook(:view_layouts_base_html_head)[0] + end + + def test_call_hook_from_view_with_project_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /project/i, view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_from_view_with_controller_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /controller/i, view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_from_view_with_request_added_to_context + @hook_module.add_listener(TestHook3) + assert_match /request/i, view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_from_view_should_join_responses_with_a_space + @hook_module.add_listener(TestHook1) + @hook_module.add_listener(TestHook2) + assert_equal 'Test hook 1 listener. Test hook 2 listener.', + view_hook_helper.call_hook(:view_layouts_base_html_head) + end + + def test_call_hook_should_not_change_the_default_url_for_email_notifications + issue = Issue.find(1) + + ActionMailer::Base.deliveries.clear + Mailer.issue_add(issue).deliver + mail = ActionMailer::Base.deliveries.last + + @hook_module.add_listener(TestLinkToHook) + hook_helper.call_hook(:view_layouts_base_html_head) + + ActionMailer::Base.deliveries.clear + Mailer.issue_add(issue).deliver + mail2 = ActionMailer::Base.deliveries.last + + assert_equal mail_body(mail), mail_body(mail2) + end + + def hook_helper + @hook_helper ||= TestHookHelperController.new + end + + def view_hook_helper + @view_hook_helper ||= TestHookHelperView.new(Rails.root.to_s + '/app/views') + end +end + diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/019777b51c435b18c756e5573fde25c903195b68.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/019777b51c435b18c756e5573fde25c903195b68.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,37 @@ +require 'rexml/document' + +module Redmine + module VERSION #:nodoc: + MAJOR = 2 + MINOR = 4 + TINY = 2 + + # Branch values: + # * official release: nil + # * stable branch: stable + # * trunk: devel + BRANCH = 'stable' + + # Retrieves the revision from the working copy + def self.revision + if File.directory?(File.join(Rails.root, '.svn')) + begin + path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s) + if `svn info --xml #{path}` =~ /revision="(\d+)"/ + return $1.to_i + end + rescue + # Could not find the current revision + end + end + nil + end + + REVISION = self.revision + ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact + STRING = ARRAY.join('.') + + def self.to_a; ARRAY end + def self.to_s; STRING end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/01b21d04c9e912327dc0059ef282ceb1a603f54f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01b21d04c9e912327dc0059ef282ceb1a603f54f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,90 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AutoCompletesControllerTest < ActionController::TestCase + fixtures :projects, :issues, :issue_statuses, + :enumerations, :users, :issue_categories, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :journals, :journal_details + + def test_issues_should_not_be_case_sensitive + get :issues, :project_id => 'ecookbook', :q => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_accept_term_param + get :issues, :project_id => 'ecookbook', :term => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_return_issue_with_given_id + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_issues_should_return_issue_with_given_id_preceded_with_hash + get :issues, :project_id => 'subproject1', :q => '#13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_with_scope_all_should_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13', :scope => 'all' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_project_should_search_all_projects + get :issues, :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_scope_all_should_not_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13' + assert_response :success + assert_equal [], assigns(:issues) + end + + def test_issues_should_return_json + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + issue = json.first + assert_kind_of Hash, issue + assert_equal 13, issue['id'] + assert_equal 13, issue['value'] + assert_equal 'Bug #13: Subproject issue two', issue['label'] + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/01/01cfbfdc38592dff3cbe5b1b36118aa0e1e65223.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01cfbfdc38592dff3cbe5b1b36118aa0e1e65223.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,122 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class GanttsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :versions + + def test_gantt_should_work + i2 = Issue.find(2) + i2.update_attribute(:due_date, 1.month.from_now) + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + # Issue with start and due dates + i = Issue.find(1) + assert_not_nil i.due_date + assert_select "div a.issue", /##{i.id}/ + # Issue with on a targeted version should not be in the events but loaded in the html + i = Issue.find(2) + assert_select "div a.issue", /##{i.id}/ + end + + def test_gantt_should_work_without_issue_due_dates + Issue.update_all("due_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_without_issue_and_version_due_dates + Issue.update_all("due_date = NULL") + Version.update_all("effective_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_cross_project + get :show + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + assert_not_nil assigns(:gantt).query + assert_nil assigns(:gantt).project + end + + def test_gantt_should_not_disclose_private_projects + get :show + assert_response :success + assert_template 'gantts/show' + assert_tag 'a', :content => /eCookbook/ + # Root private project + assert_no_tag 'a', {:content => /OnlineStore/} + # Private children of a public project + assert_no_tag 'a', :content => /Private child of eCookbook/ + end + + def test_gantt_should_display_relations + IssueRelation.delete_all + issue1 = Issue.generate!(:start_date => 1.day.from_now.to_date, :due_date => 3.day.from_now.to_date) + issue2 = Issue.generate!(:start_date => 1.day.from_now.to_date, :due_date => 3.day.from_now.to_date) + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes') + + get :show + assert_response :success + + relations = assigns(:gantt).relations + assert_kind_of Hash, relations + assert relations.present? + assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s + assert_select 'div.task_todo[id=?]:not([data-rels])', "task-todo-issue-#{issue2.id}" + end + + def test_gantt_should_export_to_pdf + get :show, :project_id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_export_to_pdf_cross_project + get :show, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + if Object.const_defined?(:Magick) + def test_gantt_should_export_to_png + get :show, :project_id => 1, :format => 'png' + assert_response :success + assert_equal 'image/png', @response.content_type + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/0235738b45d2757cc6462ce5ba2c1e87548fc84e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/0235738b45d2757cc6462ce5ba2c1e87548fc84e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,13 @@ +class CreateQueriesRoles < ActiveRecord::Migration + def self.up + create_table :queries_roles, :id => false do |t| + t.column :query_id, :integer, :null => false + t.column :role_id, :integer, :null => false + end + add_index :queries_roles, [:query_id, :role_id], :unique => true, :name => :queries_roles_ids + end + + def self.down + drop_table :queries_roles + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,943 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Helpers + # Simple class to handle gantt chart data + class Gantt + include ERB::Util + include Redmine::I18n + include Redmine::Utils::DateCalculation + + # Relation types that are rendered + DRAW_TYPES = { + IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' }, + IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' } + }.freeze + + # :nodoc: + # Some utility methods for the PDF export + class PDF + MaxCharactorsForSubject = 45 + TotalWidth = 280 + LeftPaneWidth = 100 + + def self.right_pane_width + TotalWidth - LeftPaneWidth + end + end + + attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows + attr_accessor :query + attr_accessor :project + attr_accessor :view + + def initialize(options={}) + options = options.dup + if options[:year] && options[:year].to_i >0 + @year_from = options[:year].to_i + if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 + @month_from = options[:month].to_i + else + @month_from = 1 + end + else + @month_from ||= Date.today.month + @year_from ||= Date.today.year + end + zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i + @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 + months = (options[:months] || User.current.pref[:gantt_months]).to_i + @months = (months > 0 && months < 25) ? months : 6 + # Save gantt parameters as user preference (zoom and months count) + if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || + @months != User.current.pref[:gantt_months])) + User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months + User.current.preference.save + end + @date_from = Date.civil(@year_from, @month_from, 1) + @date_to = (@date_from >> @months) - 1 + @subjects = '' + @lines = '' + @number_of_rows = nil + @issue_ancestors = [] + @truncated = false + if options.has_key?(:max_rows) + @max_rows = options[:max_rows] + else + @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i + end + end + + def common_params + { :controller => 'gantts', :action => 'show', :project_id => @project } + end + + def params + common_params.merge({:zoom => zoom, :year => year_from, + :month => month_from, :months => months}) + end + + def params_previous + common_params.merge({:year => (date_from << months).year, + :month => (date_from << months).month, + :zoom => zoom, :months => months}) + end + + def params_next + common_params.merge({:year => (date_from >> months).year, + :month => (date_from >> months).month, + :zoom => zoom, :months => months}) + end + + # Returns the number of rows that will be rendered on the Gantt chart + def number_of_rows + return @number_of_rows if @number_of_rows + rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)} + rows > @max_rows ? @max_rows : rows + end + + # Returns the number of rows that will be used to list a project on + # the Gantt chart. This will recurse for each subproject. + def number_of_rows_on_project(project) + return 0 unless projects.include?(project) + count = 1 + count += project_issues(project).size + count += project_versions(project).size + count + end + + # Renders the subjects of the Gantt chart, the left side. + def subjects(options={}) + render(options.merge(:only => :subjects)) unless @subjects_rendered + @subjects + end + + # Renders the lines of the Gantt chart, the right side + def lines(options={}) + render(options.merge(:only => :lines)) unless @lines_rendered + @lines + end + + # Returns issues that will be rendered + def issues + @issues ||= @query.issues( + :include => [:assigned_to, :tracker, :priority, :category, :fixed_version], + :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC", + :limit => @max_rows + ) + end + + # Returns a hash of the relations between the issues that are present on the gantt + # and that should be displayed, grouped by issue ids. + def relations + return @relations if @relations + if issues.any? + issue_ids = issues.map(&:id) + @relations = IssueRelation. + where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). + group_by(&:issue_from_id) + else + @relations = {} + end + end + + # Return all the project nodes that will be displayed + def projects + return @projects if @projects + ids = issues.collect(&:project).uniq.collect(&:id) + if ids.any? + # All issues projects and their visible ancestors + @projects = Project.visible.all( + :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt", + :conditions => ["child.id IN (?)", ids], + :order => "#{Project.table_name}.lft ASC" + ).uniq + else + @projects = [] + end + end + + # Returns the issues that belong to +project+ + def project_issues(project) + @issues_by_project ||= issues.group_by(&:project) + @issues_by_project[project] || [] + end + + # Returns the distinct versions of the issues that belong to +project+ + def project_versions(project) + project_issues(project).collect(&:fixed_version).compact.uniq + end + + # Returns the issues that belong to +project+ and are assigned to +version+ + def version_issues(project, version) + project_issues(project).select {|issue| issue.fixed_version == version} + end + + def render(options={}) + options = {:top => 0, :top_increment => 20, + :indent_increment => 20, :render => :subject, + :format => :html}.merge(options) + indent = options[:indent] || 4 + @subjects = '' unless options[:only] == :lines + @lines = '' unless options[:only] == :subjects + @number_of_rows = 0 + Project.project_tree(projects) do |project, level| + options[:indent] = indent + level * options[:indent_increment] + render_project(project, options) + break if abort? + end + @subjects_rendered = true unless options[:only] == :lines + @lines_rendered = true unless options[:only] == :subjects + render_end(options) + end + + def render_project(project, options={}) + subject_for_project(project, options) unless options[:only] == :lines + line_for_project(project, options) unless options[:only] == :subjects + options[:top] += options[:top_increment] + options[:indent] += options[:indent_increment] + @number_of_rows += 1 + return if abort? + issues = project_issues(project).select {|i| i.fixed_version.nil?} + sort_issues!(issues) + if issues + render_issues(issues, options) + return if abort? + end + versions = project_versions(project) + versions.each do |version| + render_version(project, version, options) + end + # Remove indent to hit the next sibling + options[:indent] -= options[:indent_increment] + end + + def render_issues(issues, options={}) + @issue_ancestors = [] + issues.each do |i| + subject_for_issue(i, options) unless options[:only] == :lines + line_for_issue(i, options) unless options[:only] == :subjects + options[:top] += options[:top_increment] + @number_of_rows += 1 + break if abort? + end + options[:indent] -= (options[:indent_increment] * @issue_ancestors.size) + end + + def render_version(project, version, options={}) + # Version header + subject_for_version(version, options) unless options[:only] == :lines + line_for_version(version, options) unless options[:only] == :subjects + options[:top] += options[:top_increment] + @number_of_rows += 1 + return if abort? + issues = version_issues(project, version) + if issues + sort_issues!(issues) + # Indent issues + options[:indent] += options[:indent_increment] + render_issues(issues, options) + options[:indent] -= options[:indent_increment] + end + end + + def render_end(options={}) + case options[:format] + when :pdf + options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) + end + end + + def subject_for_project(project, options) + case options[:format] + when :html + html_class = "" + html_class << 'icon icon-projects ' + html_class << (project.overdue? ? 'project-overdue' : '') + s = view.link_to_project(project).html_safe + subject = view.content_tag(:span, s, + :class => html_class).html_safe + html_subject(options, subject, :css => "project-name") + when :image + image_subject(options, project.name) + when :pdf + pdf_new_page?(options) + pdf_subject(options, project.name) + end + end + + def line_for_project(project, options) + # Skip versions that don't have a start_date or due date + if project.is_a?(Project) && project.start_date && project.due_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + coords = coordinates(project.start_date, project.due_date, nil, options[:zoom]) + label = h(project) + case options[:format] + when :html + html_task(options, coords, :css => "project task", :label => label, :markers => true) + when :image + image_task(options, coords, :label => label, :markers => true, :height => 3) + when :pdf + pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) + end + else + '' + end + end + + def subject_for_version(version, options) + case options[:format] + when :html + html_class = "" + html_class << 'icon icon-package ' + html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " + html_class << (version.overdue? ? 'version-overdue' : '') + html_class << ' version-closed' unless version.open? + if version.start_date && version.due_date && version.completed_pourcent + progress_date = calc_progress_date(version.start_date, + version.due_date, version.completed_pourcent) + html_class << ' behind-start-date' if progress_date < self.date_from + html_class << ' over-end-date' if progress_date > self.date_to + end + s = view.link_to_version(version).html_safe + subject = view.content_tag(:span, s, + :class => html_class).html_safe + html_subject(options, subject, :css => "version-name", + :id => "version-#{version.id}") + when :image + image_subject(options, version.to_s_with_project) + when :pdf + pdf_new_page?(options) + pdf_subject(options, version.to_s_with_project) + end + end + + def line_for_version(version, options) + # Skip versions that don't have a start_date + if version.is_a?(Version) && version.due_date && version.start_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + coords = coordinates(version.start_date, + version.due_date, version.completed_percent, + options[:zoom]) + label = "#{h version} #{h version.completed_percent.to_i.to_s}%" + label = h("#{version.project} -") + label unless @project && @project == version.project + case options[:format] + when :html + html_task(options, coords, :css => "version task", + :label => label, :markers => true, :version => version) + when :image + image_task(options, coords, :label => label, :markers => true, :height => 3) + when :pdf + pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) + end + else + '' + end + end + + def subject_for_issue(issue, options) + while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last) + @issue_ancestors.pop + options[:indent] -= options[:indent_increment] + end + output = case options[:format] + when :html + css_classes = '' + css_classes << ' issue-overdue' if issue.overdue? + css_classes << ' issue-behind-schedule' if issue.behind_schedule? + css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to + css_classes << ' issue-closed' if issue.closed? + if issue.start_date && issue.due_before && issue.done_ratio + progress_date = calc_progress_date(issue.start_date, + issue.due_before, issue.done_ratio) + css_classes << ' behind-start-date' if progress_date < self.date_from + css_classes << ' over-end-date' if progress_date > self.date_to + end + s = "".html_safe + if issue.assigned_to.present? + assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name + s << view.avatar(issue.assigned_to, + :class => 'gravatar icon-gravatar', + :size => 10, + :title => assigned_string).to_s.html_safe + end + s << view.link_to_issue(issue).html_safe + subject = view.content_tag(:span, s, :class => css_classes).html_safe + html_subject(options, subject, :css => "issue-subject", + :title => issue.subject, :id => "issue-#{issue.id}") + "\n" + when :image + image_subject(options, issue.subject) + when :pdf + pdf_new_page?(options) + pdf_subject(options, issue.subject) + end + unless issue.leaf? + @issue_ancestors << issue + options[:indent] += options[:indent_increment] + end + output + end + + def line_for_issue(issue, options) + # Skip issues that don't have a due_before (due_date or version's due_date) + if issue.is_a?(Issue) && issue.due_before + coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom]) + label = "#{issue.status.name} #{issue.done_ratio}%" + case options[:format] + when :html + html_task(options, coords, + :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), + :label => label, :issue => issue, + :markers => !issue.leaf?) + when :image + image_task(options, coords, :label => label) + when :pdf + pdf_task(options, coords, :label => label) + end + else + '' + end + end + + # Generates a gantt image + # Only defined if RMagick is avalaible + def to_image(format='PNG') + date_to = (@date_from >> @months) - 1 + show_weeks = @zoom > 1 + show_days = @zoom > 2 + subject_width = 400 + header_height = 18 + # width of one day in pixels + zoom = @zoom * 2 + g_width = (@date_to - @date_from + 1) * zoom + g_height = 20 * number_of_rows + 30 + headers_height = (show_weeks ? 2 * header_height : header_height) + height = g_height + headers_height + imgl = Magick::ImageList.new + imgl.new_image(subject_width + g_width + 1, height) + gc = Magick::Draw.new + gc.font = Redmine::Configuration['rmagick_font_path'] || "" + # Subjects + gc.stroke('transparent') + subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) + # Months headers + month_f = @date_from + left = subject_width + @months.times do + width = ((month_f >> 1) - month_f) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, 0, left + width, height) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}") + left = left + width + month_f = month_f >> 1 + end + # Weeks headers + if show_weeks + left = subject_width + height = header_height + if @date_from.cwday == 1 + # date_from is monday + week_f = date_from + else + # find next monday after date_from + week_f = @date_from + (7 - @date_from.cwday + 1) + width = (7 - @date_from.cwday + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1) + left = left + width + end + while week_f <= date_to + width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s) + left = left + width + week_f = week_f + 7 + end + end + # Days details (week-end in grey) + if show_days + left = subject_width + height = g_height + header_height - 1 + wday = @date_from.cwday + (date_to - @date_from + 1).to_i.times do + width = zoom + gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') + gc.stroke('#ddd') + gc.stroke_width(1) + gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1) + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + # border + gc.fill('transparent') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(0, 0, subject_width + g_width, headers_height) + gc.stroke('black') + gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1) + # content + top = headers_height + 20 + gc.stroke('transparent') + lines(:image => gc, :top => top, :zoom => zoom, + :subject_width => subject_width, :format => :image) + # today red line + if Date.today >= @date_from and Date.today <= date_to + gc.stroke('red') + x = (Date.today - @date_from + 1) * zoom + subject_width + gc.line(x, headers_height, x, headers_height + g_height - 1) + end + gc.draw(imgl) + imgl.format = format + imgl.to_blob + end if Object.const_defined?(:Magick) + + def to_pdf + pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) + pdf.SetTitle("#{l(:label_gantt)} #{project}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage("L") + pdf.SetFontStyle('B', 12) + pdf.SetX(15) + pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s) + pdf.Ln + pdf.SetFontStyle('B', 9) + subject_width = PDF::LeftPaneWidth + header_height = 5 + headers_height = header_height + show_weeks = false + show_days = false + if self.months < 7 + show_weeks = true + headers_height = 2 * header_height + if self.months < 3 + show_days = true + headers_height = 3 * header_height + end + end + g_width = PDF.right_pane_width + zoom = (g_width) / (self.date_to - self.date_from + 1) + g_height = 120 + t_height = g_height + headers_height + y_start = pdf.GetY + # Months headers + month_f = self.date_from + left = subject_width + height = header_height + self.months.times do + width = ((month_f >> 1) - month_f) * zoom + pdf.SetY(y_start) + pdf.SetX(left) + pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") + left = left + width + month_f = month_f >> 1 + end + # Weeks headers + if show_weeks + left = subject_width + height = header_height + if self.date_from.cwday == 1 + # self.date_from is monday + week_f = self.date_from + else + # find next monday after self.date_from + week_f = self.date_from + (7 - self.date_from.cwday + 1) + width = (7 - self.date_from.cwday + 1) * zoom-1 + pdf.SetY(y_start + header_height) + pdf.SetX(left) + pdf.RDMCell(width + 1, height, "", "LTR") + left = left + width + 1 + end + while week_f <= self.date_to + width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom + pdf.SetY(y_start + header_height) + pdf.SetX(left) + pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") + left = left + width + week_f = week_f + 7 + end + end + # Days headers + if show_days + left = subject_width + height = header_height + wday = self.date_from.cwday + pdf.SetFontStyle('B', 7) + (self.date_to - self.date_from + 1).to_i.times do + width = zoom + pdf.SetY(y_start + 2 * header_height) + pdf.SetX(left) + pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C") + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + pdf.SetY(y_start) + pdf.SetX(15) + pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1) + # Tasks + top = headers_height + y_start + options = { + :top => top, + :zoom => zoom, + :subject_width => subject_width, + :g_width => g_width, + :indent => 0, + :indent_increment => 5, + :top_increment => 5, + :format => :pdf, + :pdf => pdf + } + render(options) + pdf.Output + end + + private + + def coordinates(start_date, end_date, progress, zoom=nil) + zoom ||= @zoom + coords = {} + if start_date && end_date && start_date < self.date_to && end_date > self.date_from + if start_date > self.date_from + coords[:start] = start_date - self.date_from + coords[:bar_start] = start_date - self.date_from + else + coords[:bar_start] = 0 + end + if end_date < self.date_to + coords[:end] = end_date - self.date_from + coords[:bar_end] = end_date - self.date_from + 1 + else + coords[:bar_end] = self.date_to - self.date_from + 1 + end + if progress + progress_date = calc_progress_date(start_date, end_date, progress) + if progress_date > self.date_from && progress_date > start_date + if progress_date < self.date_to + coords[:bar_progress_end] = progress_date - self.date_from + else + coords[:bar_progress_end] = self.date_to - self.date_from + 1 + end + end + if progress_date < Date.today + late_date = [Date.today, end_date].min + if late_date > self.date_from && late_date > start_date + if late_date < self.date_to + coords[:bar_late_end] = late_date - self.date_from + 1 + else + coords[:bar_late_end] = self.date_to - self.date_from + 1 + end + end + end + end + end + # Transforms dates into pixels witdh + coords.keys.each do |key| + coords[key] = (coords[key] * zoom).floor + end + coords + end + + def calc_progress_date(start_date, end_date, progress) + start_date + (end_date - start_date + 1) * (progress / 100.0) + end + + # TODO: Sorts a collection of issues by start_date, due_date, id for gantt rendering + def sort_issues!(issues) + issues.sort! { |a, b| gantt_issue_compare(a, b) } + end + + # TODO: top level issues should be sorted by start date + def gantt_issue_compare(x, y) + if x.root_id == y.root_id + x.lft <=> y.lft + else + x.root_id <=> y.root_id + end + end + + def current_limit + if @max_rows + @max_rows - @number_of_rows + else + nil + end + end + + def abort? + if @max_rows && @number_of_rows >= @max_rows + @truncated = true + end + end + + def pdf_new_page?(options) + if options[:top] > 180 + options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) + options[:pdf].AddPage("L") + options[:top] = 15 + options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1) + end + end + + def html_subject(params, subject, options={}) + style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;" + style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width] + output = view.content_tag(:div, subject, + :class => options[:css], :style => style, + :title => options[:title], + :id => options[:id]) + @subjects << output + output + end + + def pdf_subject(params, subject, options={}) + params[:pdf].SetY(params[:top]) + params[:pdf].SetX(15) + char_limit = PDF::MaxCharactorsForSubject - params[:indent] + params[:pdf].RDMCell(params[:subject_width] - 15, 5, + (" " * params[:indent]) + + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), + "LR") + params[:pdf].SetY(params[:top]) + params[:pdf].SetX(params[:subject_width]) + params[:pdf].RDMCell(params[:g_width], 5, "", "LR") + end + + def image_subject(params, subject, options={}) + params[:image].fill('black') + params[:image].stroke('transparent') + params[:image].stroke_width(1) + params[:image].text(params[:indent], params[:top] + 2, subject) + end + + def issue_relations(issue) + rels = {} + if relations[issue.id] + relations[issue.id].each do |relation| + (rels[relation.relation_type] ||= []) << relation.issue_to_id + end + end + rels + end + + def html_task(params, coords, options={}) + output = '' + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + width = coords[:bar_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue] + html_id = "task-todo-version-#{options[:version].id}" if options[:version] + content_opt = {:style => style, + :class => "#{options[:css]} task_todo", + :id => html_id} + if options[:issue] + rels = issue_relations(options[:issue]) + if rels.present? + content_opt[:data] = {"rels" => rels.to_json} + end + end + output << view.content_tag(:div, ' '.html_safe, content_opt) + if coords[:bar_late_end] + width = coords[:bar_late_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} task_late") + end + if coords[:bar_progress_end] + width = coords[:bar_progress_end] - coords[:bar_start] - 2 + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{width}px;" + html_id = "task-done-issue-#{options[:issue].id}" if options[:issue] + html_id = "task-done-version-#{options[:version].id}" if options[:version] + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} task_done", + :id => html_id) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:start]}px;" + style << "width:15px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} marker starting") + end + if coords[:end] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:end] + params[:zoom]}px;" + style << "width:15px;" + output << view.content_tag(:div, ' '.html_safe, + :style => style, + :class => "#{options[:css]} marker ending") + end + end + # Renders the label on the right + if options[:label] + style = "" + style << "top:#{params[:top]}px;" + style << "left:#{(coords[:bar_end] || 0) + 8}px;" + style << "width:15px;" + output << view.content_tag(:div, options[:label], + :style => style, + :class => "#{options[:css]} label") + end + # Renders the tooltip + if options[:issue] && coords[:bar_start] && coords[:bar_end] + s = view.content_tag(:span, + view.render_issue_tooltip(options[:issue]).html_safe, + :class => "tip") + style = "" + style << "position: absolute;" + style << "top:#{params[:top]}px;" + style << "left:#{coords[:bar_start]}px;" + style << "width:#{coords[:bar_end] - coords[:bar_start]}px;" + style << "height:12px;" + output << view.content_tag(:div, s.html_safe, + :style => style, + :class => "tooltip") + end + @lines << output + output + end + + def pdf_task(params, coords, options={}) + height = options[:height] || 2 + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(200, 200, 200) + params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1) + if coords[:bar_late_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(255, 100, 100) + params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1) + end + if coords[:bar_progress_end] + params[:pdf].SetY(params[:top] + 1.5) + params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) + params[:pdf].SetFillColor(90, 200, 90) + params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + params[:pdf].SetY(params[:top] + 1) + params[:pdf].SetX(params[:subject_width] + coords[:start] - 1) + params[:pdf].SetFillColor(50, 50, 200) + params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) + end + if coords[:end] + params[:pdf].SetY(params[:top] + 1) + params[:pdf].SetX(params[:subject_width] + coords[:end] - 1) + params[:pdf].SetFillColor(50, 50, 200) + params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) + end + end + # Renders the label on the right + if options[:label] + params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5) + params[:pdf].RDMCell(30, 2, options[:label]) + end + end + + def image_task(params, coords, options={}) + height = options[:height] || 6 + # Renders the task bar, with progress and late + if coords[:bar_start] && coords[:bar_end] + params[:image].fill('#aaa') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_end], + params[:top] - height) + if coords[:bar_late_end] + params[:image].fill('#f66') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_late_end], + params[:top] - height) + end + if coords[:bar_progress_end] + params[:image].fill('#00c600') + params[:image].rectangle(params[:subject_width] + coords[:bar_start], + params[:top], + params[:subject_width] + coords[:bar_progress_end], + params[:top] - height) + end + end + # Renders the markers + if options[:markers] + if coords[:start] + x = params[:subject_width] + coords[:start] + y = params[:top] - height / 2 + params[:image].fill('blue') + params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + end + if coords[:end] + x = params[:subject_width] + coords[:end] + params[:zoom] + y = params[:top] - height / 2 + params[:image].fill('blue') + params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) + end + end + # Renders the label on the right + if options[:label] + params[:image].fill('black') + params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5, + params[:top] + 1, + options[:label]) + end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/027410b40ae4e2d4f4be0368cf13b4cdb0164b5f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/027410b40ae4e2d4f4be0368cf13b4cdb0164b5f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,129 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RoleTest < ActiveSupport::TestCase + fixtures :roles, :workflows, :trackers + + def test_sorted_scope + assert_equal Role.all.sort, Role.sorted.all + end + + def test_givable_scope + assert_equal Role.all.reject(&:builtin?).sort, Role.givable.all + end + + def test_builtin_scope + assert_equal Role.all.select(&:builtin?).sort, Role.builtin(true).all.sort + assert_equal Role.all.reject(&:builtin?).sort, Role.builtin(false).all.sort + end + + def test_copy_from + role = Role.find(1) + copy = Role.new.copy_from(role) + + assert_nil copy.id + assert_equal '', copy.name + assert_equal role.permissions, copy.permissions + + copy.name = 'Copy' + assert copy.save + end + + def test_copy_workflows + source = Role.find(1) + assert_equal 90, source.workflow_rules.size + + target = Role.new(:name => 'Target') + assert target.save + target.workflow_rules.copy(source) + target.reload + assert_equal 90, target.workflow_rules.size + end + + def test_permissions_should_be_unserialized_with_its_coder + Role::PermissionsAttributeCoder.expects(:load).once + Role.find(1).permissions + end + + def test_add_permission + role = Role.find(1) + size = role.permissions.size + role.add_permission!("apermission", "anotherpermission") + role.reload + assert role.permissions.include?(:anotherpermission) + assert_equal size + 2, role.permissions.size + end + + def test_remove_permission + role = Role.find(1) + size = role.permissions.size + perm = role.permissions[0..1] + role.remove_permission!(*perm) + role.reload + assert ! role.permissions.include?(perm[0]) + assert_equal size - 2, role.permissions.size + end + + def test_name + I18n.locale = 'fr' + assert_equal 'Manager', Role.find(1).name + assert_equal 'Anonyme', Role.anonymous.name + assert_equal 'Non membre', Role.non_member.name + end + + def test_find_all_givable + assert_equal Role.all.reject(&:builtin?).sort, Role.find_all_givable + end + + def test_anonymous_should_return_the_anonymous_role + assert_no_difference('Role.count') do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin + end + end + + def test_anonymous_with_a_missing_anonymous_role_should_return_the_anonymous_role + Role.where(:builtin => Role::BUILTIN_ANONYMOUS).delete_all + + assert_difference('Role.count') do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin + end + end + + def test_non_member_should_return_the_non_member_role + assert_no_difference('Role.count') do + role = Role.non_member + assert role.builtin? + assert_equal Role::BUILTIN_NON_MEMBER, role.builtin + end + end + + def test_non_member_with_a_missing_non_member_role_should_return_the_non_member_role + Role.where(:builtin => Role::BUILTIN_NON_MEMBER).delete_all + + assert_difference('Role.count') do + role = Role.non_member + assert role.builtin? + assert_equal Role::BUILTIN_NON_MEMBER, role.builtin + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/02c68441083bdea630158440f0e7d9d62ff8f790.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/02c68441083bdea630158440f0e7d9d62ff8f790.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,822 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class MailHandlerTest < ActiveSupport::TestCase + fixtures :users, :projects, :enabled_modules, :roles, + :members, :member_roles, :users, + :issues, :issue_statuses, + :workflows, :trackers, :projects_trackers, + :versions, :enumerations, :issue_categories, + :custom_fields, :custom_fields_trackers, :custom_fields_projects, + :boards, :messages + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' + + def setup + ActionMailer::Base.deliveries.clear + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) + end + + def teardown + Setting.clear_cache + end + + def test_add_issue + ActionMailer::Base.deliveries.clear + # This email contains: 'Project: onlinestore' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Project.find(2), issue.project + assert_equal issue.project.trackers.first, issue.tracker + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal IssueStatus.find_by_name('Resolved'), issue.status + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + assert_equal '2010-01-01', issue.start_date.to_s + assert_equal '2010-12-31', issue.due_date.to_s + assert_equal User.find_by_login('jsmith'), issue.assigned_to + assert_equal Version.find_by_name('Alpha'), issue.fixed_version + assert_equal 2.5, issue.estimated_hours + assert_equal 30, issue.done_ratio + assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] + # keywords should be removed from the email body + assert !issue.description.match(/^Project:/i) + assert !issue.description.match(/^Status:/i) + assert !issue.description.match(/^Start Date:/i) + # Email notification should be sent + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert mail.subject.include?('New ticket on a given project') + end + + def test_add_issue_with_default_tracker + # This email contains: 'Project: onlinestore' + issue = submit_email( + 'ticket_on_given_project.eml', + :issue => {:tracker => 'Support request'} + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'Support request', issue.tracker.name + end + + def test_add_issue_with_status + # This email contains: 'Project: onlinestore' and 'Status: Resolved' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Project.find(2), issue.project + assert_equal IssueStatus.find_by_name("Resolved"), issue.status + end + + def test_add_issue_with_attributes_override + issue = submit_email( + 'ticket_with_attributes.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_group_assignment + with_settings :issue_group_assignment => '1' do + issue = submit_email('ticket_on_given_project.eml') do |email| + email.gsub!('Assigned to: John Smith', 'Assigned to: B Team') + end + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Group.find(11), issue.assigned_to + end + end + + def test_add_issue_with_partial_attributes_override + issue = submit_email( + 'ticket_with_attributes.eml', + :issue => {:priority => 'High'}, + :allow_override => ['tracker'] + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_nil issue.category + assert_equal 'High', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_spaces_between_attribute_and_separator + issue = submit_email( + 'ticket_with_spaces_between_attribute_and_separator.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_attachment_to_specific_project + issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'Ticket created by email with attachment', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'This is a new ticket with attachments', issue.description + # Attachment properties + assert_equal 1, issue.attachments.size + assert_equal 'Paella.jpg', issue.attachments.first.filename + assert_equal 'image/jpeg', issue.attachments.first.content_type + assert_equal 10790, issue.attachments.first.filesize + end + + def test_add_issue_with_custom_fields + issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket with custom field values', issue.subject + assert_equal 'PostgreSQL', issue.custom_field_value(1) + assert_equal 'Value for a custom field', issue.custom_field_value(2) + assert !issue.description.match(/^searchable field:/i) + end + + def test_add_issue_with_version_custom_fields + field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3]) + + issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email| + email << "Affected version: 1.0\n" + end + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal '2', issue.custom_field_value(field) + end + + def test_add_issue_should_match_assignee_on_display_name + user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz') + User.add_to_project(user, Project.find(2)) + issue = submit_email('ticket_on_given_project.eml') do |email| + email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz') + end + assert issue.is_a?(Issue) + assert_equal user, issue.assigned_to + end + + def test_add_issue_with_cc + issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo')) + assert_equal 1, issue.watcher_user_ids.size + end + + def test_add_issue_by_unknown_user + assert_no_difference 'User.count' do + assert_equal false, + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'} + ) + end + end + + def test_add_issue_by_anonymous_user + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + issue = submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'accept' + ) + assert issue.is_a?(Issue) + assert issue.author.anonymous? + end + end + + def test_add_issue_by_anonymous_user_with_no_from_address + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + issue = submit_email( + 'ticket_by_empty_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'accept' + ) + assert issue.is_a?(Issue) + assert issue.author.anonymous? + end + end + + def test_add_issue_by_anonymous_user_on_private_project + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + assert_no_difference 'Issue.count' do + assert_equal false, + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'onlinestore'}, + :unknown_user => 'accept' + ) + end + end + end + + def test_add_issue_by_anonymous_user_on_private_project_without_permission_check + assert_no_difference 'User.count' do + assert_difference 'Issue.count' do + issue = submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'onlinestore'}, + :no_permission_check => '1', + :unknown_user => 'accept' + ) + assert issue.is_a?(Issue) + assert issue.author.anonymous? + assert !issue.project.is_public? + assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] + end + end + end + + def test_add_issue_by_created_user + Setting.default_language = 'en' + assert_difference 'User.count' do + issue = submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create' + ) + assert issue.is_a?(Issue) + assert issue.author.active? + assert_equal 'john.doe@somenet.foo', issue.author.mail + assert_equal 'John', issue.author.firstname + assert_equal 'Doe', issue.author.lastname + + # account information + email = ActionMailer::Base.deliveries.first + assert_not_nil email + assert email.subject.include?('account activation') + login = mail_body(email).match(/\* Login: (.*)$/)[1].strip + password = mail_body(email).match(/\* Password: (.*)$/)[1].strip + assert_equal issue.author, User.try_to_login(login, password) + end + end + + def test_created_user_should_be_added_to_groups + group1 = Group.generate! + group2 = Group.generate! + + assert_difference 'User.count' do + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create', + :default_group => "#{group1.name},#{group2.name}" + ) + end + user = User.order('id DESC').first + assert_same_elements [group1, group2], user.groups + end + + def test_created_user_should_not_receive_account_information_with_no_account_info_option + assert_difference 'User.count' do + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create', + :no_account_notice => '1' + ) + end + + # only 1 email for the new issue notification + assert_equal 1, ActionMailer::Base.deliveries.size + email = ActionMailer::Base.deliveries.first + assert_include 'Ticket by unknown user', email.subject + end + + def test_created_user_should_have_mail_notification_to_none_with_no_notification_option + assert_difference 'User.count' do + submit_email( + 'ticket_by_unknown_user.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create', + :no_notification => '1' + ) + end + user = User.order('id DESC').first + assert_equal 'none', user.mail_notification + end + + def test_add_issue_without_from_header + Role.anonymous.add_permission!(:add_issues) + assert_equal false, submit_email('ticket_without_from_header.eml') + end + + def test_add_issue_with_invalid_attributes + issue = submit_email( + 'ticket_with_invalid_attributes.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_nil issue.assigned_to + assert_nil issue.start_date + assert_nil issue.due_date + assert_equal 0, issue.done_ratio + assert_equal 'Normal', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_localized_attributes + User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr' + issue = submit_email( + 'ticket_with_localized_attributes.eml', + :allow_override => 'tracker,category,priority' + ) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_japanese_keywords + ja_dev = "\xe9\x96\x8b\xe7\x99\xba" + ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding) + tracker = Tracker.create!(:name => ja_dev) + Project.find(1).trackers << tracker + issue = submit_email( + 'japanese_keywords_iso_2022_jp.eml', + :issue => {:project => 'ecookbook'}, + :allow_override => 'tracker' + ) + assert_kind_of Issue, issue + assert_equal tracker, issue.tracker + end + + def test_add_issue_from_apple_mail + issue = submit_email( + 'apple_mail_with_attachment.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + + attachment = issue.attachments.first + assert_equal 'paella.jpg', attachment.filename + assert_equal 10790, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 10790, File.size(attachment.diskfile) + assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest + end + + def test_thunderbird_with_attachment_ja + issue = submit_email( + 'thunderbird_with_attachment_ja.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + attachment = issue.attachments.first + assert_equal ja, attachment.filename + assert_equal 5, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 5, File.size(attachment.diskfile) + assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest + end + + def test_gmail_with_attachment_ja + issue = submit_email( + 'gmail_with_attachment_ja.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + attachment = issue.attachments.first + assert_equal ja, attachment.filename + assert_equal 5, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 5, File.size(attachment.diskfile) + assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest + end + + def test_thunderbird_with_attachment_latin1 + issue = submit_email( + 'thunderbird_with_attachment_iso-8859-1.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + u = "" + u.force_encoding('UTF-8') if u.respond_to?(:force_encoding) + u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" + u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding) + 11.times { u << u1 } + attachment = issue.attachments.first + assert_equal "#{u}.png", attachment.filename + assert_equal 130, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 130, File.size(attachment.diskfile) + assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest + end + + def test_gmail_with_attachment_latin1 + issue = submit_email( + 'gmail_with_attachment_iso-8859-1.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal 1, issue.attachments.size + u = "" + u.force_encoding('UTF-8') if u.respond_to?(:force_encoding) + u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" + u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding) + 11.times { u << u1 } + attachment = issue.attachments.first + assert_equal "#{u}.txt", attachment.filename + assert_equal 5, attachment.filesize + assert File.exist?(attachment.diskfile) + assert_equal 5, File.size(attachment.diskfile) + assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest + end + + def test_add_issue_with_iso_8859_1_subject + issue = submit_email( + 'subject_as_iso-8859-1.eml', + :issue => {:project => 'ecookbook'} + ) + str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..." + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + assert_kind_of Issue, issue + assert_equal str, issue.subject + end + + def test_add_issue_with_japanese_subject + issue = submit_email( + 'subject_japanese_1.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + assert_equal ja, issue.subject + end + + def test_add_issue_with_no_subject_header + issue = submit_email( + 'no_subject_header.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + assert_equal '(no subject)', issue.subject + end + + def test_add_issue_with_mixed_japanese_subject + issue = submit_email( + 'subject_japanese_2.eml', + :issue => {:project => 'ecookbook'} + ) + assert_kind_of Issue, issue + ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" + ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding) + assert_equal ja, issue.subject + end + + def test_should_ignore_emails_from_locked_users + User.find(2).lock! + + MailHandler.any_instance.expects(:dispatch).never + assert_no_difference 'Issue.count' do + assert_equal false, submit_email('ticket_on_given_project.eml') + end + end + + def test_should_ignore_emails_from_emission_address + Role.anonymous.add_permission!(:add_issues) + assert_no_difference 'User.count' do + assert_equal false, + submit_email( + 'ticket_from_emission_address.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create' + ) + end + end + + def test_should_ignore_auto_replied_emails + MailHandler.any_instance.expects(:dispatch).never + [ + "X-Auto-Response-Suppress: OOF", + "Auto-Submitted: auto-replied", + "Auto-Submitted: Auto-Replied", + "Auto-Submitted: auto-generated" + ].each do |header| + raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + raw = header + "\n" + raw + + assert_no_difference 'Issue.count' do + assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored" + end + end + end + + def test_add_issue_should_send_email_notification + Setting.notified_events = ['issue_added'] + ActionMailer::Base.deliveries.clear + # This email contains: 'Project: onlinestore' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_update_issue + journal = submit_email('ticket_reply.eml') + assert journal.is_a?(Journal) + assert_equal User.find_by_login('jsmith'), journal.user + assert_equal Issue.find(2), journal.journalized + assert_match /This is reply/, journal.notes + assert_equal false, journal.private_notes + assert_equal 'Feature request', journal.issue.tracker.name + end + + def test_update_issue_with_attribute_changes + # This email contains: 'Status: Resolved' + journal = submit_email('ticket_reply_with_status.eml') + assert journal.is_a?(Journal) + issue = Issue.find(journal.issue.id) + assert_equal User.find_by_login('jsmith'), journal.user + assert_equal Issue.find(2), journal.journalized + assert_match /This is reply/, journal.notes + assert_equal 'Feature request', journal.issue.tracker.name + assert_equal IssueStatus.find_by_name("Resolved"), issue.status + assert_equal '2010-01-01', issue.start_date.to_s + assert_equal '2010-12-31', issue.due_date.to_s + assert_equal User.find_by_login('jsmith'), issue.assigned_to + assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value + # keywords should be removed from the email body + assert !journal.notes.match(/^Status:/i) + assert !journal.notes.match(/^Start Date:/i) + end + + def test_update_issue_with_attachment + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + assert_difference 'Attachment.count' do + assert_no_difference 'Issue.count' do + journal = submit_email('ticket_with_attachment.eml') do |raw| + raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories' + end + end + end + end + end + journal = Journal.first(:order => 'id DESC') + assert_equal Issue.find(2), journal.journalized + assert_equal 1, journal.details.size + + detail = journal.details.first + assert_equal 'attachment', detail.property + assert_equal 'Paella.jpg', detail.value + end + + def test_update_issue_should_send_email_notification + ActionMailer::Base.deliveries.clear + journal = submit_email('ticket_reply.eml') + assert journal.is_a?(Journal) + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_update_issue_should_not_set_defaults + journal = submit_email( + 'ticket_reply.eml', + :issue => {:tracker => 'Support request', :priority => 'High'} + ) + assert journal.is_a?(Journal) + assert_match /This is reply/, journal.notes + assert_equal 'Feature request', journal.issue.tracker.name + assert_equal 'Normal', journal.issue.priority.name + end + + def test_replying_to_a_private_note_should_add_reply_as_private + private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2) + + assert_difference 'Journal.count' do + journal = submit_email('ticket_reply.eml') do |email| + email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: " + end + + assert_kind_of Journal, journal + assert_match /This is reply/, journal.notes + assert_equal true, journal.private_notes + end + end + + def test_reply_to_a_message + m = submit_email('message_reply.eml') + assert m.is_a?(Message) + assert !m.new_record? + m.reload + assert_equal 'Reply via email', m.subject + # The email replies to message #2 which is part of the thread of message #1 + assert_equal Message.find(1), m.parent + end + + def test_reply_to_a_message_by_subject + m = submit_email('message_reply_by_subject.eml') + assert m.is_a?(Message) + assert !m.new_record? + m.reload + assert_equal 'Reply to the first post', m.subject + assert_equal Message.find(1), m.parent + end + + def test_should_strip_tags_of_html_only_emails + issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'HTML email', issue.subject + assert_equal 'This is a html-only email.', issue.description + 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 + + 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 + + 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 + + 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 + + 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 + + def test_email_with_long_subject_line + issue = submit_email('ticket_with_long_subject.eml') + assert issue.is_a?(Issue) + assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255] + end + + def test_new_user_from_attributes_should_return_valid_user + to_test = { + # [address, name] => [login, firstname, lastname] + ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'], + ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'], + ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'], + ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'], + ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'], + ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh'] + } + + to_test.each do |attrs, expected| + user = MailHandler.new_user_from_attributes(attrs.first, attrs.last) + + assert user.valid?, user.errors.full_messages.to_s + assert_equal attrs.first, user.mail + assert_equal expected[0], user.login + assert_equal expected[1], user.firstname + assert_equal expected[2], user.lastname + assert_equal 'only_my_events', user.mail_notification + 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 + end + end + + def test_new_user_from_attributes_should_use_default_login_if_invalid + user = MailHandler.new_user_from_attributes('foo+bar@example.net') + assert user.valid? + assert user.login =~ /^user[a-f0-9]+$/ + assert_equal 'foo+bar@example.net', user.mail + end + + def test_new_user_with_utf8_encoded_fullname_should_be_decoded + assert_difference 'User.count' do + issue = submit_email( + 'fullname_of_sender_as_utf8_encoded.eml', + :issue => {:project => 'ecookbook'}, + :unknown_user => 'create' + ) + end + + user = User.first(:order => 'id DESC') + assert_equal "foo@example.org", user.mail + str1 = "\xc3\x84\xc3\xa4" + str2 = "\xc3\x96\xc3\xb6" + str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding) + str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding) + assert_equal str1, user.firstname + assert_equal str2, user.lastname + end + + private + + def submit_email(filename, options={}) + raw = IO.read(File.join(FIXTURES_PATH, filename)) + yield raw if block_given? + MailHandler.receive(raw, options) + end + + def assert_issue_created(issue) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/02/02fe391dd14e50d29ad45f8bf8dea30be02556c5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/02fe391dd14e50d29ad45f8bf8dea30be02556c5.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,127 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Themes + + # Return an array of installed themes + def self.themes + @@installed_themes ||= scan_themes + end + + # Rescan themes directory + def self.rescan + @@installed_themes = scan_themes + end + + # Return theme for given id, or nil if it's not found + def self.theme(id, options={}) + return nil if id.blank? + + found = themes.find {|t| t.id == id} + if found.nil? && options[:rescan] != false + rescan + found = theme(id, :rescan => false) + end + found + end + + # Class used to represent a theme + class Theme + attr_reader :path, :name, :dir + + def initialize(path) + @path = path + @dir = File.basename(path) + @name = @dir.humanize + @stylesheets = nil + @javascripts = nil + end + + # Directory name used as the theme id + def id; dir end + + def ==(theme) + theme.is_a?(Theme) && theme.dir == dir + end + + def <=>(theme) + name <=> theme.name + end + + def stylesheets + @stylesheets ||= assets("stylesheets", "css") + end + + def images + @images ||= assets("images") + end + + def javascripts + @javascripts ||= assets("javascripts", "js") + end + + def stylesheet_path(source) + "/themes/#{dir}/stylesheets/#{source}" + end + + def image_path(source) + "/themes/#{dir}/images/#{source}" + end + + def javascript_path(source) + "/themes/#{dir}/javascripts/#{source}" + end + + private + + def assets(dir, ext=nil) + if ext + Dir.glob("#{path}/#{dir}/*.#{ext}").collect {|f| File.basename(f).gsub(/\.#{ext}$/, '')} + else + Dir.glob("#{path}/#{dir}/*").collect {|f| File.basename(f)} + end + end + end + + private + + def self.scan_themes + dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f| + # A theme should at least override application.css + File.directory?(f) && File.exist?("#{f}/stylesheets/application.css") + end + dirs.collect {|dir| Theme.new(dir)}.sort + end + end +end + +module ApplicationHelper + def current_theme + unless instance_variable_defined?(:@current_theme) + @current_theme = Redmine::Themes.theme(Setting.ui_theme) + end + @current_theme + end + + # Returns the header tags for the current theme + def heads_for_theme + if current_theme && current_theme.javascripts.include?('theme') + javascript_include_tag current_theme.javascript_path('theme') + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/03/0327b87f817dd6e7b29c9fc60cf3afcc96b39ebb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/0327b87f817dd6e7b29c9fc60cf3afcc96b39ebb.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/03/033d1080f94d03c5b655f6847c2f7606bd5d5f60.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/033d1080f94d03c5b655f6847c2f7606bd5d5f60.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,54 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module UsersHelper + def users_status_options_for_select(selected) + user_count_by_status = User.count(:group => 'status').to_hash + options_for_select([[l(:label_all), ''], + ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'], + ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'], + ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s) + end + + def user_mail_notification_options(user) + user.valid_notification_options.collect {|o| [l(o.last), o.first]} + end + + def change_status_link(user) + url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil} + + if user.locked? + link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' + elsif user.registered? + link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' + elsif user != User.current + link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock' + end + end + + def user_settings_tabs + tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, + {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} + ] + if Group.all.any? + tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural} + end + tabs + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/03/03b977fa002b4a1e6202e05fed1b5c5f4bee2ed3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03b977fa002b4a1e6202e05fed1b5c5f4bee2ed3.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1029 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Project < ActiveRecord::Base + include Redmine::SafeAttributes + + # Project statuses + STATUS_ACTIVE = 1 + STATUS_CLOSED = 5 + STATUS_ARCHIVED = 9 + + # Maximum length for project identifiers + IDENTIFIER_MAX_LENGTH = 100 + + # Specific overidden Activities + has_many :time_entry_activities + has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}" + has_many :memberships, :class_name => 'Member' + has_many :member_principals, :class_name => 'Member', + :include => :principal, + :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})" + has_many :users, :through => :members + has_many :principals, :through => :member_principals, :source => :principal + + has_many :enabled_modules, :dependent => :delete_all + has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" + has_many :issues, :dependent => :destroy, :include => [:status, :tracker] + has_many :issue_changes, :through => :issues, :source => :journals + has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" + has_many :time_entries, :dependent => :delete_all + has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all + has_many :documents, :dependent => :destroy + has_many :news, :dependent => :destroy, :include => :author + has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" + has_many :boards, :dependent => :destroy, :order => "position ASC" + has_one :repository, :conditions => ["is_default = ?", true] + has_many :repositories, :dependent => :destroy + has_many :changesets, :through => :repository + has_one :wiki, :dependent => :destroy + # Custom field for the project issues + has_and_belongs_to_many :issue_custom_fields, + :class_name => 'IssueCustomField', + :order => "#{CustomField.table_name}.position", + :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", + :association_foreign_key => 'custom_field_id' + + acts_as_nested_set :order => 'name', :dependent => :destroy + acts_as_attachable :view_permission => :view_files, + :delete_permission => :manage_files + + acts_as_customizable + acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil + acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, + :author => nil + + attr_protected :status + + validates_presence_of :name, :identifier + validates_uniqueness_of :identifier + validates_associated :repository, :wiki + validates_length_of :name, :maximum => 255 + validates_length_of :homepage, :maximum => 255 + validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH + # donwcase letters, digits, dashes but not digits only + validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? } + # reserved words + validates_exclusion_of :identifier, :in => %w( new ) + + after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} + after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} + before_destroy :delete_all_members + + scope :has_module, lambda {|mod| + where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s) + } + scope :active, lambda { where(:status => STATUS_ACTIVE) } + scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } + scope :all_public, lambda { where(:is_public => true) } + scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) } + scope :allowed_to, lambda {|*args| + user = User.current + permission = nil + if args.first.is_a?(Symbol) + permission = args.shift + else + user = args.shift + permission = args.shift + end + where(Project.allowed_to_condition(user, permission, *args)) + } + scope :like, lambda {|arg| + if arg.blank? + where(nil) + else + pattern = "%#{arg.to_s.strip.downcase}%" + where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern) + end + } + + def initialize(attributes=nil, *args) + super + + initialized = (attributes || {}).stringify_keys + if !initialized.key?('identifier') && Setting.sequential_project_identifiers? + self.identifier = Project.next_identifier + end + if !initialized.key?('is_public') + self.is_public = Setting.default_projects_public? + end + if !initialized.key?('enabled_module_names') + self.enabled_module_names = Setting.default_projects_modules + end + if !initialized.key?('trackers') && !initialized.key?('tracker_ids') + default = Setting.default_projects_tracker_ids + if default.is_a?(Array) + self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all + else + self.trackers = Tracker.sorted.all + end + end + end + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].blank? && !(new_record? || identifier.blank?) + end + + # returns latest created projects + # non public projects will be returned only if user is a member of those + def self.latest(user=nil, count=5) + visible(user).limit(count).order("created_on DESC").all + end + + # Returns true if the project is visible to +user+ or to the current user. + def visible?(user=User.current) + user.allowed_to?(:view_project, self) + end + + # Returns a SQL conditions string used to find all projects visible by the specified user. + # + # Examples: + # Project.visible_condition(admin) => "projects.status = 1" + # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))" + # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))" + def self.visible_condition(user, options={}) + allowed_to_condition(user, :view_project, options) + end + + # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ + # + # Valid options: + # * :project => limit the condition to project + # * :with_subprojects => limit the condition to project and its subprojects + # * :member => limit the condition to the user projects + def self.allowed_to_condition(user, permission, options={}) + perm = Redmine::AccessControl.permission(permission) + base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") + if perm && perm.project_module + # If the permission belongs to a project module, make sure the module is enabled + base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" + end + if options[:project] + project_statement = "#{Project.table_name}.id = #{options[:project].id}" + project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] + base_statement = "(#{project_statement}) AND (#{base_statement})" + end + + if user.admin? + base_statement + else + statement_by_role = {} + unless options[:member] + role = user.logged? ? Role.non_member : Role.anonymous + if role.allowed_to?(permission) + statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" + end + end + if user.logged? + user.projects_by_role.each do |role, projects| + if role.allowed_to?(permission) && projects.any? + statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" + end + end + end + if statement_by_role.empty? + "1=0" + else + if block_given? + statement_by_role.each do |role, statement| + if s = yield(role, user) + statement_by_role[role] = "(#{statement} AND (#{s}))" + end + end + end + "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))" + end + end + end + + # Returns the Systemwide and project specific activities + def activities(include_inactive=false) + if include_inactive + return all_activities + else + return active_activities + end + end + + # Will create a new Project specific Activity or update an existing one + # + # This will raise a ActiveRecord::Rollback if the TimeEntryActivity + # does not successfully save. + def update_or_create_time_entry_activity(id, activity_hash) + if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id') + self.create_time_entry_activity_if_needed(activity_hash) + else + activity = project.time_entry_activities.find_by_id(id.to_i) + activity.update_attributes(activity_hash) if activity + end + end + + # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity + # + # This will raise a ActiveRecord::Rollback if the TimeEntryActivity + # does not successfully save. + def create_time_entry_activity_if_needed(activity) + if activity['parent_id'] + + parent_activity = TimeEntryActivity.find(activity['parent_id']) + activity['name'] = parent_activity.name + activity['position'] = parent_activity.position + + if Enumeration.overridding_change?(activity, parent_activity) + project_activity = self.time_entry_activities.create(activity) + + if project_activity.new_record? + raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved" + else + self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id]) + end + end + end + end + + # Returns a :conditions SQL string that can be used to find the issues associated with this project. + # + # Examples: + # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))" + # project.project_condition(false) => "projects.id = 1" + def project_condition(with_subprojects) + cond = "#{Project.table_name}.id = #{id}" + cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects + cond + end + + def self.find(*args) + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + project = find_by_identifier(*args) + raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil? + project + else + super + end + end + + def self.find_by_param(*args) + self.find(*args) + end + + alias :base_reload :reload + def reload(*args) + @shared_versions = nil + @rolled_up_versions = nil + @rolled_up_trackers = nil + @all_issue_custom_fields = nil + @all_time_entry_custom_fields = nil + @to_param = nil + @allowed_parents = nil + @allowed_permissions = nil + @actions_allowed = nil + @start_date = nil + @due_date = nil + base_reload(*args) + end + + def to_param + # id is used for projects with a numeric identifier (compatibility) + @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) + end + + def active? + self.status == STATUS_ACTIVE + end + + def archived? + self.status == STATUS_ARCHIVED + end + + # Archives the project and its descendants + def archive + # Check that there is no issue of a non descendant project that is assigned + # to one of the project or descendant versions + v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten + if v_ids.any? && + Issue. + includes(:project). + where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt). + where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids). + exists? + return false + end + Project.transaction do + archive! + end + true + end + + # Unarchives the project + # All its ancestors must be active + def unarchive + return false if ancestors.detect {|a| !a.active?} + update_attribute :status, STATUS_ACTIVE + end + + def close + self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED + end + + def reopen + self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE + end + + # Returns an array of projects the project can be moved to + # by the current user + def allowed_parents + return @allowed_parents if @allowed_parents + @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all + @allowed_parents = @allowed_parents - self_and_descendants + if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?) + @allowed_parents << nil + end + unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent) + @allowed_parents << parent + end + @allowed_parents + end + + # Sets the parent of the project with authorization check + def set_allowed_parent!(p) + unless p.nil? || p.is_a?(Project) + if p.to_s.blank? + p = nil + else + p = Project.find_by_id(p) + return false unless p + end + end + if p.nil? + if !new_record? && allowed_parents.empty? + return false + end + elsif !allowed_parents.include?(p) + return false + end + set_parent!(p) + end + + # Sets the parent of the project + # Argument can be either a Project, a String, a Fixnum or nil + def set_parent!(p) + unless p.nil? || p.is_a?(Project) + if p.to_s.blank? + p = nil + else + p = Project.find_by_id(p) + return false unless p + end + end + if p == parent && !p.nil? + # Nothing to do + true + elsif p.nil? || (p.active? && move_possible?(p)) + set_or_update_position_under(p) + Issue.update_versions_from_hierarchy_change(self) + true + else + # Can not move to the given target + false + end + end + + # Recalculates all lft and rgt values based on project names + # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid + # Used in BuildProjectsTree migration + def self.rebuild_tree! + transaction do + update_all "lft = NULL, rgt = NULL" + rebuild!(false) + end + end + + # Returns an array of the trackers used by the project and its active sub projects + def rolled_up_trackers + @rolled_up_trackers ||= + Tracker. + joins(:projects). + joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'"). + select("DISTINCT #{Tracker.table_name}.*"). + where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt). + sorted. + all + end + + # Closes open and locked project versions that are completed + def close_completed_versions + Version.transaction do + versions.where(:status => %w(open locked)).all.each do |version| + if version.completed? + version.update_attribute(:status, 'closed') + end + end + end + end + + # Returns a scope of the Versions on subprojects + def rolled_up_versions + @rolled_up_versions ||= + Version.scoped(:include => :project, + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt]) + end + + # Returns a scope of the Versions used by the project + def shared_versions + if new_record? + Version.scoped(:include => :project, + :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'") + else + @shared_versions ||= begin + r = root? ? self : root + Version.scoped(:include => :project, + :conditions => "#{Project.table_name}.id = #{id}" + + " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" + + " #{Version.table_name}.sharing = 'system'" + + " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + + " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + + " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + + "))") + end + end + end + + # Returns a hash of project users grouped by role + def users_by_role + members.includes(:user, :roles).all.inject({}) do |h, m| + m.roles.each do |r| + h[r] ||= [] + h[r] << m.user + end + h + end + end + + # Deletes all project's members + def delete_all_members + me, mr = Member.table_name, MemberRole.table_name + connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})") + Member.delete_all(['project_id = ?', id]) + end + + # Users/groups issues can be assigned to + def assignable_users + assignable = Setting.issue_group_assignment? ? member_principals : members + assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort + end + + # Returns the mail adresses of users that should be always notified on project events + def recipients + notified_users.collect {|user| user.mail} + end + + # Returns the users that should be notified on project events + def notified_users + # TODO: User part should be extracted to User#notify_about? + members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal} + end + + # Returns an array of all custom fields enabled for project issues + # (explictly associated custom fields and custom fields enabled for all projects) + def all_issue_custom_fields + @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort + end + + # Returns an array of all custom fields enabled for project time entries + # (explictly associated custom fields and custom fields enabled for all projects) + def all_time_entry_custom_fields + @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort + end + + def project + self + end + + def <=>(project) + name.downcase <=> project.name.downcase + end + + def to_s + name + end + + # Returns a short description of the projects (first lines) + def short_description(length = 255) + description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description + end + + def css_classes + s = 'project' + s << ' root' if root? + s << ' child' if child? + s << (leaf? ? ' leaf' : ' parent') + unless active? + if archived? + s << ' archived' + else + s << ' closed' + end + end + s + end + + # The earliest start date of a project, based on it's issues and versions + def start_date + @start_date ||= [ + issues.minimum('start_date'), + shared_versions.minimum('effective_date'), + Issue.fixed_version(shared_versions).minimum('start_date') + ].compact.min + end + + # The latest due date of an issue or version + def due_date + @due_date ||= [ + issues.maximum('due_date'), + shared_versions.maximum('effective_date'), + Issue.fixed_version(shared_versions).maximum('due_date') + ].compact.max + end + + def overdue? + active? && !due_date.nil? && (due_date < Date.today) + end + + # Returns the percent completed for this project, based on the + # progress on it's versions. + def completed_percent(options={:include_subprojects => false}) + if options.delete(:include_subprojects) + total = self_and_descendants.collect(&:completed_percent).sum + + total / self_and_descendants.count + else + if versions.count > 0 + total = versions.collect(&:completed_percent).sum + + total / versions.count + else + 100 + end + end + end + + # Return true if this project allows to do the specified action. + # action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + def allows_to?(action) + if archived? + # No action allowed on archived projects + return false + end + unless active? || Redmine::AccessControl.read_action?(action) + # No write action allowed on closed projects + return false + end + # No action allowed on disabled modules + if action.is_a? Hash + allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + else + allowed_permissions.include? action + end + end + + def module_enabled?(module_name) + module_name = module_name.to_s + enabled_modules.detect {|m| m.name == module_name} + end + + def enabled_module_names=(module_names) + if module_names && module_names.is_a?(Array) + module_names = module_names.collect(&:to_s).reject(&:blank?) + self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)} + else + enabled_modules.clear + end + end + + # Returns an array of the enabled modules names + def enabled_module_names + enabled_modules.collect(&:name) + end + + # Enable a specific module + # + # Examples: + # project.enable_module!(:issue_tracking) + # project.enable_module!("issue_tracking") + def enable_module!(name) + enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name) + end + + # Disable a module if it exists + # + # Examples: + # project.disable_module!(:issue_tracking) + # project.disable_module!("issue_tracking") + # project.disable_module!(project.enabled_modules.first) + def disable_module!(target) + target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target) + target.destroy unless target.blank? + end + + safe_attributes 'name', + 'description', + 'homepage', + 'is_public', + 'identifier', + 'custom_field_values', + 'custom_fields', + 'tracker_ids', + 'issue_custom_field_ids' + + safe_attributes 'enabled_module_names', + :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } + + safe_attributes 'inherit_members', + :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)} + + # Returns an array of projects that are in this project's hierarchy + # + # Example: parents, children, siblings + def hierarchy + parents = project.self_and_ancestors || [] + descendants = project.descendants || [] + project_hierarchy = parents | descendants # Set union + end + + # Returns an auto-generated project identifier based on the last identifier used + def self.next_identifier + p = Project.order('id DESC').first + p.nil? ? nil : p.identifier.to_s.succ + end + + # Copies and saves the Project instance based on the +project+. + # Duplicates the source project's: + # * Wiki + # * Versions + # * Categories + # * Issues + # * Members + # * Queries + # + # Accepts an +options+ argument to specify what to copy + # + # Examples: + # project.copy(1) # => copies everything + # project.copy(1, :only => 'members') # => copies members only + # project.copy(1, :only => ['members', 'versions']) # => copies members and versions + def copy(project, options={}) + project = project.is_a?(Project) ? project : Project.find(project) + + to_be_copied = %w(wiki versions issue_categories issues members queries boards) + to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil? + + Project.transaction do + if save + reload + to_be_copied.each do |name| + send "copy_#{name}", project + end + Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self) + save + end + end + end + + # Returns a new unsaved Project instance with attributes copied from +project+ + def self.copy_from(project) + project = project.is_a?(Project) ? project : Project.find(project) + # clear unique attributes + attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt') + copy = Project.new(attributes) + copy.enabled_modules = project.enabled_modules + copy.trackers = project.trackers + copy.custom_values = project.custom_values.collect {|v| v.clone} + copy.issue_custom_fields = project.issue_custom_fields + copy + end + + # Yields the given block for each project with its level in the tree + def self.project_tree(projects, &block) + ancestors = [] + projects.sort_by(&:lft).each do |project| + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield project, ancestors.size + ancestors << project + end + end + + private + + def after_parent_changed(parent_was) + remove_inherited_member_roles + add_inherited_member_roles + end + + def update_inherited_members + if parent + if inherit_members? && !inherit_members_was + remove_inherited_member_roles + add_inherited_member_roles + elsif !inherit_members? && inherit_members_was + remove_inherited_member_roles + end + end + end + + def remove_inherited_member_roles + member_roles = memberships.map(&:member_roles).flatten + member_role_ids = member_roles.map(&:id) + member_roles.each do |member_role| + if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from) + member_role.destroy + end + end + end + + def add_inherited_member_roles + if inherit_members? && parent + parent.memberships.each do |parent_member| + member = Member.find_or_new(self.id, parent_member.user_id) + parent_member.member_roles.each do |parent_member_role| + member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id) + end + member.save! + end + end + end + + # Copies wiki from +project+ + def copy_wiki(project) + # Check that the source project has a wiki first + unless project.wiki.nil? + wiki = self.wiki || Wiki.new + wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") + wiki_pages_map = {} + project.wiki.pages.each do |page| + # Skip pages without content + next if page.content.nil? + new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on")) + new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id")) + new_wiki_page.content = new_wiki_content + wiki.pages << new_wiki_page + wiki_pages_map[page.id] = new_wiki_page + end + + self.wiki = wiki + wiki.save + # Reproduce page hierarchy + project.wiki.pages.each do |page| + if page.parent_id && wiki_pages_map[page.id] + wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id] + wiki_pages_map[page.id].save + end + end + end + end + + # Copies versions from +project+ + def copy_versions(project) + project.versions.each do |version| + new_version = Version.new + new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on") + self.versions << new_version + end + end + + # Copies issue categories from +project+ + def copy_issue_categories(project) + project.issue_categories.each do |issue_category| + new_issue_category = IssueCategory.new + new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id") + self.issue_categories << new_issue_category + end + end + + # Copies issues from +project+ + def copy_issues(project) + # Stores the source issue id as a key and the copied issues as the + # value. Used to map the two togeather for issue relations. + issues_map = {} + + # Store status and reopen locked/closed versions + version_statuses = versions.reject(&:open?).map {|version| [version, version.status]} + version_statuses.each do |version, status| + version.update_attribute :status, 'open' + end + + # Get issues sorted by root_id, lft so that parent issues + # get copied before their children + project.issues.reorder('root_id, lft').all.each do |issue| + new_issue = Issue.new + new_issue.copy_from(issue, :subtasks => false, :link => false) + new_issue.project = self + # Changing project resets the custom field values + # TODO: handle this in Issue#project= + new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} + # Reassign fixed_versions by name, since names are unique per project + if issue.fixed_version && issue.fixed_version.project == project + new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name} + end + # Reassign the category by name, since names are unique per project + if issue.category + new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name} + end + # Parent issue + if issue.parent_id + if copied_parent = issues_map[issue.parent_id] + new_issue.parent_issue_id = copied_parent.id + end + end + + self.issues << new_issue + if new_issue.new_record? + logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info + else + issues_map[issue.id] = new_issue unless new_issue.new_record? + end + end + + # Restore locked/closed version statuses + version_statuses.each do |version, status| + version.update_attribute :status, status + end + + # Relations after in case issues related each other + project.issues.each do |issue| + new_issue = issues_map[issue.id] + unless new_issue + # Issue was not copied + next + end + + # Relations + issue.relations_from.each do |source_relation| + new_issue_relation = IssueRelation.new + new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") + new_issue_relation.issue_to = issues_map[source_relation.issue_to_id] + if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations? + new_issue_relation.issue_to = source_relation.issue_to + end + new_issue.relations_from << new_issue_relation + end + + issue.relations_to.each do |source_relation| + new_issue_relation = IssueRelation.new + new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") + new_issue_relation.issue_from = issues_map[source_relation.issue_from_id] + if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations? + new_issue_relation.issue_from = source_relation.issue_from + end + new_issue.relations_to << new_issue_relation + end + end + end + + # Copies members from +project+ + def copy_members(project) + # Copy users first, then groups to handle members with inherited and given roles + members_to_copy = [] + members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)} + members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)} + + members_to_copy.each do |member| + new_member = Member.new + new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") + # only copy non inherited roles + # inherited roles will be added when copying the group membership + role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) + next if role_ids.empty? + new_member.role_ids = role_ids + new_member.project = self + self.members << new_member + end + end + + # Copies queries from +project+ + def copy_queries(project) + project.queries.each do |query| + new_query = IssueQuery.new + new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") + new_query.sort_criteria = query.sort_criteria if query.sort_criteria + new_query.project = self + new_query.user_id = query.user_id + self.queries << new_query + end + end + + # Copies boards from +project+ + def copy_boards(project) + project.boards.each do |board| + new_board = Board.new + new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id") + new_board.project = self + self.boards << new_board + end + end + + def allowed_permissions + @allowed_permissions ||= begin + module_names = enabled_modules.all(:select => :name).collect {|m| m.name} + Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name} + end + end + + def allowed_actions + @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten + end + + # Returns all the active Systemwide and project specific activities + def active_activities + overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) + + if overridden_activity_ids.empty? + return TimeEntryActivity.shared.active + else + return system_activities_and_project_overrides + end + end + + # Returns all the Systemwide and project specific activities + # (inactive and active) + def all_activities + overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) + + if overridden_activity_ids.empty? + return TimeEntryActivity.shared + else + return system_activities_and_project_overrides(true) + end + end + + # Returns the systemwide active activities merged with the project specific overrides + def system_activities_and_project_overrides(include_inactive=false) + if include_inactive + return TimeEntryActivity.shared. + where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all + + self.time_entry_activities + else + return TimeEntryActivity.shared.active. + where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all + + self.time_entry_activities.active + end + end + + # Archives subprojects recursively + def archive! + children.each do |subproject| + subproject.send :archive! + end + update_attribute :status, STATUS_ARCHIVED + end + + def update_position_under_parent + set_or_update_position_under(parent) + end + + # Inserts/moves the project so that target's children or root projects stay alphabetically sorted + def set_or_update_position_under(target_parent) + parent_was = parent + sibs = (target_parent.nil? ? self.class.roots : target_parent.children) + to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } + + if to_be_inserted_before + move_to_left_of(to_be_inserted_before) + elsif target_parent.nil? + if sibs.empty? + # move_to_root adds the project in first (ie. left) position + move_to_root + else + move_to_right_of(sibs.last) unless self == sibs.last + end + else + # move_to_child_of adds the project in last (ie.right) position + move_to_child_of(target_parent) + end + if parent_was != target_parent + after_parent_changed(parent_was) + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/04/0490719dc3185896fb07e210967a75c0f5e40a0a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/0490719dc3185896fb07e210967a75c0f5e40a0a.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,54 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueTransactionTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :trackers, :projects_trackers, + :versions, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries + + self.use_transactional_fixtures = false + + def test_invalid_move_to_another_project + parent1 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + grandchild = Issue.generate!(:parent_issue_id => child.id, :tracker_id => 2) + Project.find(2).tracker_ids = [1] + + parent1.reload + assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + + # child can not be moved to Project 2 because its child is on a disabled tracker + child = Issue.find(child.id) + child.project = Project.find(2) + assert !child.save + child.reload + grandchild.reload + parent1.reload + + # no change + assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt] + assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/04/049234b460fae5eb30eba8a2487d0eac09c6701f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/049234b460fae5eb30eba8a2487d0eac09c6701f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,174 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +namespace :redmine do + namespace :email do + + desc <<-END_DESC +Read an email from standard input. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + no_account_notice=1 disable new user account notification + default_group=foo,bar adds created user to foo and bar groups + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + rake redmine:email:read RAILS_ENV="production" < raw_email + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + rake redmine:email:read RAILS_ENV="production" \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority < raw_email +END_DESC + + task :read => :environment do + MailHandler.receive(STDIN.read, MailHandler.extract_options_from_env(ENV)) + end + + desc <<-END_DESC +Read emails from an IMAP server. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + no_account_notice=1 disable new user account notification + default_group=foo,bar adds created user to foo and bar groups + +Available IMAP options: + host=HOST IMAP server host (default: 127.0.0.1) + port=PORT IMAP server port (default: 143) + ssl=SSL Use SSL? (default: false) + username=USERNAME IMAP account + password=PASSWORD IMAP password + folder=FOLDER IMAP folder to read (default: INBOX) + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Processed emails control options: + move_on_success=MAILBOX move emails that were successfully received + to MAILBOX instead of deleting them + move_on_failure=MAILBOX move emails that were ignored to MAILBOX + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx + + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority +END_DESC + + task :receive_imap => :environment do + imap_options = {:host => ENV['host'], + :port => ENV['port'], + :ssl => ENV['ssl'], + :username => ENV['username'], + :password => ENV['password'], + :folder => ENV['folder'], + :move_on_success => ENV['move_on_success'], + :move_on_failure => ENV['move_on_failure']} + + Redmine::IMAP.check(imap_options, MailHandler.extract_options_from_env(ENV)) + end + + desc <<-END_DESC +Read emails from an POP3 server. + +Available POP3 options: + host=HOST POP3 server host (default: 127.0.0.1) + port=PORT POP3 server port (default: 110) + username=USERNAME POP3 account + password=PASSWORD POP3 password + apop=1 use APOP authentication (default: false) + delete_unprocessed=1 delete messages that could not be processed + successfully from the server (default + behaviour is to leave them on the server) + +See redmine:email:receive_imap for more options and examples. +END_DESC + + task :receive_pop3 => :environment do + pop_options = {:host => ENV['host'], + :port => ENV['port'], + :apop => ENV['apop'], + :username => ENV['username'], + :password => ENV['password'], + :delete_unprocessed => ENV['delete_unprocessed']} + + Redmine::POP3.check(pop_options, MailHandler.extract_options_from_env(ENV)) + end + + desc "Send a test email to the user with the provided login name" + task :test, [:login] => :environment do |task, args| + include Redmine::I18n + abort l(:notice_email_error, "Please include the user login to test with. Example: rake redmine:email:test[login]") if args[:login].blank? + + user = User.find_by_login(args[:login]) + abort l(:notice_email_error, "User #{args[:login]} not found") unless user && user.logged? + + ActionMailer::Base.raise_delivery_errors = true + begin + Mailer.with_synched_deliveries do + Mailer.test_email(user).deliver + end + puts l(:notice_email_sent, user.mail) + rescue Exception => e + abort l(:notice_email_error, e.message) + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/04/04dcbe1c1303811a184d8f2171e27c9cdde0c767.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/04dcbe1c1303811a184d8f2171e27c9cdde0c767.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1091 @@ +fa: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: rtl + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y/%m/%d" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [یک‌شنبه, دوشنبه, سه‌شنبه, چهارشنبه, پنج‌شنبه, آدینه, شنبه] + abbr_day_names: [یک, دو, سه, چهار, پنج, آدینه, شنبه] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, ژانویه, Ùوریه, مارس, آوریل, مه, ژوئن, ژوئیه, اوت, سپتامبر, اکتبر, نوامبر, دسامبر] + abbr_month_names: [~, ژان, Ùور, مار, آور, مه, ژوئن, ژوئیه, اوت, سپت, اکت, نوا, دسا] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y/%m/%d - %H:%M" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%d %B %Y ساعت %H:%M" + am: "صبح" + pm: "عصر" + + datetime: + distance_in_words: + half_a_minute: "نیم دقیقه" + less_than_x_seconds: + one: "کمتر از 1 ثانیه" + other: "کمتر از %{count} ثانیه" + x_seconds: + one: "1 ثانیه" + other: "%{count} ثانیه" + less_than_x_minutes: + one: "کمتر از 1 دقیقه" + other: "کمتر از %{count} دقیقه" + x_minutes: + one: "1 دقیقه" + other: "%{count} دقیقه" + about_x_hours: + one: "نزدیک 1 ساعت" + other: "نزدیک %{count} ساعت" + x_hours: + one: "1 ساعت" + other: "%{count} ساعت" + x_days: + one: "1 روز" + other: "%{count} روز" + about_x_months: + one: "نزدیک 1 ماه" + other: "نزدیک %{count} ماه" + x_months: + one: "1 ماه" + other: "%{count} ماه" + about_x_years: + one: "نزدیک 1 سال" + other: "نزدیک %{count} سال" + over_x_years: + one: "بیش از 1 سال" + other: "بیش از %{count} سال" + almost_x_years: + one: "نزدیک 1 سال" + other: "نزدیک %{count} سال" + + number: + # Default format for numbers + format: + separator: "Ù«" + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "بایت" + other: "بایت" + kb: "کیلوبایت" + mb: "مگابایت" + gb: "گیگابایت" + tb: "ترابایت" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "Ùˆ" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 ایراد از ذخیره سازی این %{model} جلوگیری کرد" + other: "%{count} ایراد از ذخیره سازی این %{model} جلوگیری کرد" + messages: + inclusion: "در Ùهرست نیامده است" + exclusion: "رزرو شده است" + invalid: "نادرست است" + confirmation: "با بررسی سازگاری ندارد" + accepted: "باید Ù¾Ø°ÛŒØ±ÙØªÙ‡ شود" + empty: "نمی‌تواند تهی باشد" + blank: "نمی‌تواند تهی باشد" + too_long: "خیلی بلند است (بیشترین اندازه %{count} نویسه است)" + too_short: "خیلی کوتاه است (کمترین اندازه %{count} نویسه است)" + wrong_length: "اندازه نادرست است (باید %{count} نویسه باشد)" + taken: "پیش از این Ú¯Ø±ÙØªÙ‡ شده است" + not_a_number: "شماره درستی نیست" + not_a_date: "تاریخ درستی نیست" + greater_than: "باید بزرگتر از %{count} باشد" + greater_than_or_equal_to: "باید بزرگتر از یا برابر با %{count} باشد" + equal_to: "باید برابر با %{count} باشد" + less_than: "باید کمتر از %{count} باشد" + less_than_or_equal_to: "باید کمتر از یا برابر با %{count} باشد" + odd: "باید ÙØ±Ø¯ باشد" + even: "باید زوج باشد" + greater_than_start_date: "باید از تاریخ آغاز بزرگتر باشد" + not_same_project: "به همان پروژه وابسته نیست" + circular_dependency: "این وابستگی یک وابستگی دایره وار خواهد ساخت" + cant_link_an_issue_with_a_descendant: "یک پیامد نمی‌تواند به یکی از زیر کارهایش پیوند بخورد" + + actionview_instancetag_blank_option: گزینش کنید + + general_text_No: 'خیر' + general_text_Yes: 'آری' + general_text_no: 'خیر' + general_text_yes: 'آری' + general_lang_name: 'Persian (پارسی)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '6' + + notice_account_updated: حساب شما بروز شد. + notice_account_invalid_creditentials: نام کاربری یا گذرواژه نادرست است + notice_account_password_updated: گذرواژه بروز شد + notice_account_wrong_password: گذرواژه نادرست است + notice_account_register_done: حساب ساخته شد. برای ÙØ¹Ø§Ù„ نمودن آن، روی پیوندی Ú©Ù‡ به شما ایمیل شده کلیک کنید. + notice_account_unknown_email: کاربر شناخته نشد. + notice_can_t_change_password: این حساب یک روش شناسایی بیرونی را به کار Ú¯Ø±ÙØªÙ‡ است. گذرواژه را نمی‌توان جایگزین کرد. + notice_account_lost_email_sent: یک ایمیل با راهنمایی درباره گزینش گذرواژه تازه برای شما ÙØ±Ø³ØªØ§Ø¯Ù‡ شد. + notice_account_activated: حساب شما ÙØ¹Ø§Ù„ شده است. اکنون می‌توانید وارد شوید. + notice_successful_create: با موÙقیت ساخته شد. + notice_successful_update: با موÙقیت بروز شد. + notice_successful_delete: با موÙقیت برداشته شد. + notice_successful_connection: با موÙقیت متصل شد. + notice_file_not_found: برگه درخواستی شما در دسترس نیست یا پاک شده است. + notice_locking_conflict: داده‌ها را کاربر دیگری بروز کرده است. + notice_not_authorized: شما به این برگه دسترسی ندارید. + notice_not_authorized_archived_project: پروژه درخواستی شما بایگانی شده است. + notice_email_sent: "یک ایمیل به %{value} ÙØ±Ø³ØªØ§Ø¯Ù‡ شد." + notice_email_error: "یک ایراد در ÙØ±Ø³ØªØ§Ø¯Ù† ایمیل پیش آمد (%{value})." + notice_feeds_access_key_reseted: کلید دسترسی RSS شما بازنشانی شد. + notice_api_access_key_reseted: کلید دسترسی API شما بازنشانی شد. + notice_failed_to_save_issues: "ذخیره سازی %{count} پیامد از %{total} پیامد گزینش شده شکست خورد: %{ids}." + notice_failed_to_save_members: "ذخیره سازی اعضا شکست خورد: %{errors}." + notice_no_issue_selected: "هیچ پیامدی برگزیده نشده است! پیامدهایی Ú©Ù‡ می‌خواهید ویرایش کنید را برگزینید." + notice_account_pending: "حساب شما ساخته شد Ùˆ اکنون چشم به راه روادید سرپرست است." + notice_default_data_loaded: پیکربندی پیش‌گزیده با موÙقیت بار شد. + notice_unable_delete_version: نگارش را نمی‌توان پاک کرد. + notice_unable_delete_time_entry: زمان گزارش شده را نمی‌توان پاک کرد. + notice_issue_done_ratios_updated: اندازه انجام شده پیامد بروز شد. + notice_gantt_chart_truncated: "نمودار بریده شد چون از بیشترین شماری Ú©Ù‡ می‌توان نشان داد بزگتر است (%{max})." + + error_can_t_load_default_data: "پیکربندی پیش‌گزیده نمی‌تواند بار شود: %{value}" + error_scm_not_found: "بخش یا نگارش در انباره پیدا نشد." + error_scm_command_failed: "ایرادی در دسترسی به انباره پیش آمد: %{value}" + error_scm_annotate: "بخش پیدا نشد یا نمی‌توان برای آن یادداشت نوشت." + error_issue_not_found_in_project: 'پیامد پیدا نشد یا به این پروژه وابسته نیست.' + error_no_tracker_in_project: 'هیچ پیگردی به این پروژه پیوسته نشده است. پیکربندی پروژه را بررسی کنید.' + error_no_default_issue_status: 'هیچ وضعیت پیامد پیش‌گزیده‌ای مشخص نشده است. پیکربندی را بررسی کنید (به «پیکربندی -> وضعیت‌های پیامد» بروید).' + error_can_not_delete_custom_field: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ را نمی‌توان پاک کرد. + error_can_not_delete_tracker: "این پیگرد دارای پیامد است Ùˆ نمی‌توان آن را پاک کرد." + error_can_not_remove_role: "این نقش به کار Ú¯Ø±ÙØªÙ‡ شده است Ùˆ نمی‌توان آن را پاک کرد." + error_can_not_reopen_issue_on_closed_version: 'یک پیامد Ú©Ù‡ به یک نگارش بسته شده وابسته است را نمی‌توان باز کرد.' + error_can_not_archive_project: این پروژه را نمی‌توان بایگانی کرد. + error_issue_done_ratios_not_updated: "اندازه انجام شده پیامد بروز نشد." + error_workflow_copy_source: 'یک پیگرد یا نقش منبع را برگزینید.' + error_workflow_copy_target: 'پیگردها یا نقش‌های مقصد را برگزینید.' + error_unable_delete_issue_status: 'وضعیت پیامد را نمی‌توان پاک کرد.' + error_unable_to_connect: "نمی‌توان متصل شد (%{value})" + warning_attachments_not_saved: "%{count} پرونده ذخیره نشد." + + mail_subject_lost_password: "گذرواژه حساب %{value} شما" + mail_body_lost_password: 'برای جایگزینی گذرواژه خود، بر روی پیوند زیر کلیک کنید:' + mail_subject_register: "ÙØ¹Ø§Ù„سازی حساب %{value} شما" + mail_body_register: 'برای ÙØ¹Ø§Ù„سازی حساب خود، بر روی پیوند زیر کلیک کنید:' + mail_body_account_information_external: "شما می‌توانید حساب %{value} خود را برای ورود به کار برید." + mail_body_account_information: داده‌های حساب شما + mail_subject_account_activation_request: "درخواست ÙØ¹Ø§Ù„سازی حساب %{value}" + mail_body_account_activation_request: "یک کاربر تازه (%{value}) نامنویسی کرده است. این حساب چشم به راه روادید شماست:" + mail_subject_reminder: "زمان رسیدگی به %{count} پیامد در %{days} روز آینده سر می‌رسد" + mail_body_reminder: "زمان رسیدگی به %{count} پیامد Ú©Ù‡ به شما واگذار شده است، در %{days} روز آینده سر می‌رسد:" + mail_subject_wiki_content_added: "برگه ویکی «%{id}» Ø§ÙØ²ÙˆØ¯Ù‡ شد" + mail_body_wiki_content_added: "برگه ویکی «%{id}» به دست %{author} Ø§ÙØ²ÙˆØ¯Ù‡ شد." + mail_subject_wiki_content_updated: "برگه ویکی «%{id}» بروز شد" + mail_body_wiki_content_updated: "برگه ویکی «%{id}» به دست %{author} بروز شد." + + + field_name: نام + field_description: یادداشت + field_summary: خلاصه + field_is_required: الزامی + field_firstname: نام Ú©ÙˆÚ†Ú© + field_lastname: نام خانوادگی + field_mail: ایمیل + field_filename: پرونده + field_filesize: اندازه + field_downloads: Ø¯Ø±ÛŒØ§ÙØªâ€ŒÙ‡Ø§ + field_author: نویسنده + field_created_on: ساخته شده در + field_updated_on: بروز شده در + field_field_format: قالب + field_is_for_all: برای همه پروژه‌ها + field_possible_values: مقادیر ممکن + field_regexp: عبارت منظم + field_min_length: کمترین اندازه + field_max_length: بیشترین اندازه + field_value: مقدار + field_category: دسته + field_title: عنوان + field_project: پروژه + field_issue: پیامد + field_status: وضعیت + field_notes: یادداشت + field_is_closed: پیامد بسته شده + field_is_default: مقدار پیش‌گزیده + field_tracker: پیگرد + field_subject: موضوع + field_due_date: زمان سررسید + field_assigned_to: واگذار شده به + field_priority: برتری + field_fixed_version: نگارش هد٠+ field_user: کاربر + field_principal: دستور دهنده + field_role: نقش + field_homepage: برگه خانه + field_is_public: همگانی + field_parent: پروژه پدر + field_is_in_roadmap: این پیامدها در چشم‌انداز نشان داده شوند + field_login: ورود + field_mail_notification: آگاه سازی‌های ایمیلی + field_admin: سرپرست + field_last_login_on: آخرین ورود + field_language: زبان + field_effective_date: تاریخ + field_password: گذرواژه + field_new_password: گذرواژه تازه + field_password_confirmation: بررسی گذرواژه + field_version: نگارش + field_type: گونه + field_host: میزبان + field_port: درگاه + field_account: حساب + field_base_dn: DN پایه + field_attr_login: نشانه ورود + field_attr_firstname: نشانه نام Ú©ÙˆÚ†Ú© + field_attr_lastname: نشانه نام خانوادگی + field_attr_mail: نشانه ایمیل + field_onthefly: ساخت کاربر بیدرنگ + field_start_date: تاریخ آغاز + field_done_ratio: Ùª انجام شده + field_auth_source: روش شناسایی + field_hide_mail: ایمیل من پنهان شود + field_comments: دیدگاه + field_url: نشانی + field_start_page: برگه آغاز + field_subproject: زیر پروژه + field_hours: ساعت‌ + field_activity: گزارش + field_spent_on: در تاریخ + field_identifier: شناسه + field_is_filter: پالایش پذیر + field_issue_to: پیامد وابسته + field_delay: دیرکرد + field_assignable: پیامدها می‌توانند به این نقش واگذار شوند + field_redirect_existing_links: پیوندهای پیشین به پیوند تازه راهنمایی شوند + field_estimated_hours: زمان برآورد شده + field_column_names: ستون‌ها + field_time_entries: زمان نوشتن + field_time_zone: پهنه زمانی + field_searchable: جستجو پذیر + field_default_value: مقدار پیش‌گزیده + field_comments_sorting: نمایش دیدگاه‌ها + field_parent_title: برگه پدر + field_editable: ویرایش پذیر + field_watcher: دیده‌بان + field_identity_url: نشانی OpenID + field_content: محتوا + field_group_by: دسته بندی با + field_sharing: اشتراک گذاری + field_parent_issue: کار پدر + field_member_of_group: "دسته واگذار شونده" + field_assigned_to_role: "نقش واگذار شونده" + field_text: Ùیلد متنی + field_visible: آشکار + + setting_app_title: نام برنامه + setting_app_subtitle: زیرنام برنامه + setting_welcome_text: نوشتار خوش‌آمد گویی + setting_default_language: زبان پیش‌گزیده + setting_login_required: الزامی بودن ورود + setting_self_registration: خود نام نویسی + setting_attachment_max_size: بیشترین اندازه پیوست + setting_issues_export_limit: کرانه صدور پییامدها + setting_mail_from: نشانی ÙØ±Ø³ØªÙ†Ø¯Ù‡ ایمیل + setting_bcc_recipients: گیرندگان ایمیل دیده نشوند (bcc) + setting_plain_text_mail: ایمیل نوشته ساده (بدون HTML) + setting_host_name: نام میزبان Ùˆ نشانی + setting_text_formatting: قالب بندی نوشته + setting_wiki_compression: ÙØ´Ø±Ø¯Ù‡â€ŒØ³Ø§Ø²ÛŒ پیشینه ویکی + setting_feeds_limit: کرانه محتوای خوراک + setting_default_projects_public: حالت پیش‌گزیده پروژه‌های تازه، همگانی است + setting_autofetch_changesets: Ø¯Ø±ÛŒØ§ÙØª خودکار تغییرات + setting_sys_api_enabled: ÙØ¹Ø§Ù„ سازی وب سرویس برای سرپرستی انباره + setting_commit_ref_keywords: کلیدواژه‌های نشانه + setting_commit_fix_keywords: کلیدواژه‌های انجام + setting_autologin: ورود خودکار + setting_date_format: قالب تاریخ + setting_time_format: قالب زمان + setting_cross_project_issue_relations: توانایی وابستگی میان پروژه‌ای پیامدها + setting_issue_list_default_columns: ستون‌های پیش‌گزیده نمایش داده شده در Ùهرست پیامدها + setting_emails_header: سرنویس ایمیل‌ها + setting_emails_footer: پانویس ایمیل‌ها + setting_protocol: پیوندنامه + setting_per_page_options: گزینه‌های اندازه داده‌های هر برگ + setting_user_format: قالب نمایشی کاربران + setting_activity_days_default: روزهای نمایش داده شده در گزارش پروژه + setting_display_subprojects_issues: پیش‌گزیده نمایش پیامدهای زیرپروژه در پروژه پدر + setting_enabled_scm: ÙØ¹Ø§Ù„سازی SCM + setting_mail_handler_body_delimiters: "بریدن ایمیل‌ها پس از یکی از این ردیÙ‌ها" + setting_mail_handler_api_enabled: ÙØ¹Ø§Ù„سازی وب سرویس برای ایمیل‌های آمده + setting_mail_handler_api_key: کلید API + setting_sequential_project_identifiers: ساخت پشت سر هم شناسه پروژه + setting_gravatar_enabled: کاربرد Gravatar برای عکس کاربر + setting_gravatar_default: عکس Gravatar پیش‌گزیده + setting_diff_max_lines_displayed: بیشترین اندازه ردیÙ‌های ØªÙØ§ÙˆØª نشان داده شده + setting_file_max_size_displayed: بیشترین اندازه پرونده‌های نمایش داده شده درون خطی + setting_repository_log_display_limit: بیشترین شمار نگارش‌های نمایش داده شده در گزارش پرونده + setting_openid: پذیرش ورود Ùˆ نام نویسی با OpenID + setting_password_min_length: کمترین اندازه گذرواژه + setting_new_project_user_role_id: نقش داده شده به کاربری Ú©Ù‡ سرپرست نیست Ùˆ پروژه می‌سازد + setting_default_projects_modules: پیمانه‌های پیش‌گزیده ÙØ¹Ø§Ù„ برای پروژه‌های تازه + setting_issue_done_ratio: برآورد اندازه انجام شده پیامد با + setting_issue_done_ratio_issue_field: کاربرد Ùیلد پیامد + setting_issue_done_ratio_issue_status: کاربرد وضعیت پیامد + setting_start_of_week: آغاز گاهشمار از + setting_rest_api_enabled: ÙØ¹Ø§Ù„سازی وب سرویس‌های REST + setting_cache_formatted_text: نهان سازی نوشته‌های قالب بندی شده + setting_default_notification_option: آگاه سازی پیش‌گزیده + setting_commit_logtime_enabled: ÙØ¹Ø§Ù„سازی زمان گذاشته شده + setting_commit_logtime_activity_id: کار زمان گذاشته شده + setting_gantt_items_limit: بیشترین شمار بخش‌های نمایش داده شده در نمودار گانت + + permission_add_project: ساخت پروژه + permission_add_subprojects: ساخت زیرپروژه + permission_edit_project: ویرایش پروژه + permission_select_project_modules: گزینش پیمانه‌های پروژه + permission_manage_members: سرپرستی اعضا + permission_manage_project_activities: سرپرستی کارهای پروژه + permission_manage_versions: سرپرستی نگارش‌ها + permission_manage_categories: سرپرستی دسته‌های پیامد + permission_view_issues: دیدن پیامدها + permission_add_issues: Ø§ÙØ²ÙˆØ¯Ù† پیامدها + permission_edit_issues: ویرایش پیامدها + permission_manage_issue_relations: سرپرستی وابستگی پیامدها + permission_add_issue_notes: Ø§ÙØ²ÙˆØ¯Ù† یادداشت + permission_edit_issue_notes: ویرایش یادداشت + permission_edit_own_issue_notes: ویرایش یادداشت خود + permission_move_issues: جابجایی پیامدها + permission_delete_issues: پاک کردن پیامدها + permission_manage_public_queries: سرپرستی پرس‌وجوهای همگانی + permission_save_queries: ذخیره سازی پرس‌وجوها + permission_view_gantt: دیدن نمودار گانت + permission_view_calendar: دیدن گاهشمار + permission_view_issue_watchers: دیدن Ùهرست دیده‌بان‌ها + permission_add_issue_watchers: Ø§ÙØ²ÙˆØ¯Ù† دیده‌بان‌ها + permission_delete_issue_watchers: پاک کردن دیده‌بان‌ها + permission_log_time: نوشتن زمان گذاشته شده + permission_view_time_entries: دیدن زمان گذاشته شده + permission_edit_time_entries: ویرایش زمان گذاشته شده + permission_edit_own_time_entries: ویرایش زمان گذاشته شده خود + permission_manage_news: سرپرستی رویدادها + permission_comment_news: گذاشتن دیدگاه روی رویدادها + permission_view_documents: دیدن نوشتارها + permission_manage_files: سرپرستی پرونده‌ها + permission_view_files: دیدن پرونده‌ها + permission_manage_wiki: سرپرستی ویکی + permission_rename_wiki_pages: نامگذاری برگه ویکی + permission_delete_wiki_pages: پاک کردن برگه ویکی + permission_view_wiki_pages: دیدن ویکی + permission_view_wiki_edits: دیدن پیشینه ویکی + permission_edit_wiki_pages: ویرایش برگه‌های ویکی + permission_delete_wiki_pages_attachments: پاک کردن پیوست‌های برگه ویکی + permission_protect_wiki_pages: نگه‌داری برگه‌های ویکی + permission_manage_repository: سرپرستی انباره + permission_browse_repository: چریدن در انباره + permission_view_changesets: دیدن تغییرات + permission_commit_access: دسترسی تغییر انباره + permission_manage_boards: سرپرستی انجمن‌ها + permission_view_messages: دیدن پیام‌ها + permission_add_messages: ÙØ±Ø³ØªØ§Ø¯Ù† پیام‌ها + permission_edit_messages: ویرایش پیام‌ها + permission_edit_own_messages: ویرایش پیام خود + permission_delete_messages: پاک کردن پیام‌ها + permission_delete_own_messages: پاک کردن پیام خود + permission_export_wiki_pages: صدور برگه‌های ویکی + permission_manage_subtasks: سرپرستی زیرکارها + + project_module_issue_tracking: پیگیری پیامدها + project_module_time_tracking: پیگیری زمان + project_module_news: رویدادها + project_module_documents: نوشتارها + project_module_files: پرونده‌ها + project_module_wiki: ویکی + project_module_repository: انباره + project_module_boards: انجمن‌ها + project_module_calendar: گاهشمار + project_module_gantt: گانت + + label_user: کاربر + label_user_plural: کاربر + label_user_new: کاربر تازه + label_user_anonymous: ناشناس + label_project: پروژه + label_project_new: پروژه تازه + label_project_plural: پروژه + label_x_projects: + zero: بدون پروژه + one: "1 پروژه" + other: "%{count} پروژه" + label_project_all: همه پروژه‌ها + label_project_latest: آخرین پروژه‌ها + label_issue: پیامد + label_issue_new: پیامد تازه + label_issue_plural: پیامد + label_issue_view_all: دیدن همه پیامدها + label_issues_by: "دسته‌بندی پیامدها با %{value}" + label_issue_added: پیامد Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_issue_updated: پیامد بروز شد + label_document: نوشتار + label_document_new: نوشتار تازه + label_document_plural: نوشتار + label_document_added: نوشتار Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_role: نقش + label_role_plural: نقش + label_role_new: نقش تازه + label_role_and_permissions: نقش‌ها Ùˆ پروانه‌ها + label_member: عضو + label_member_new: عضو تازه + label_member_plural: عضو + label_tracker: پیگرد + label_tracker_plural: پیگرد + label_tracker_new: پیگرد تازه + label_workflow: گردش کار + label_issue_status: وضعیت پیامد + label_issue_status_plural: وضعیت پیامد + label_issue_status_new: وضعیت تازه + label_issue_category: دسته پیامد + label_issue_category_plural: دسته پیامد + label_issue_category_new: دسته تازه + label_custom_field: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ + label_custom_field_plural: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ + label_custom_field_new: Ùیلد Ø³ÙØ§Ø±Ø´ÛŒ تازه + label_enumerations: برشمردنی‌ها + label_enumeration_new: مقدار تازه + label_information: داده + label_information_plural: داده + label_please_login: وارد شوید + label_register: نام نویسی کنید + label_login_with_open_id_option: یا با OpenID وارد شوید + label_password_lost: Ø¨Ø§Ø²ÛŒØ§ÙØª گذرواژه + label_home: سرآغاز + label_my_page: برگه من + label_my_account: حساب من + label_my_projects: پروژه‌های من + label_my_page_block: بخش برگه من + label_administration: سرپرستی + label_login: ورود + label_logout: خروج + label_help: راهنما + label_reported_issues: پیامدهای گزارش شده + label_assigned_to_me_issues: پیامدهای واگذار شده به من + label_last_login: آخرین ورود + label_registered_on: نام نویسی شده در + label_activity: گزارش + label_overall_activity: گزارش روی هم Ø±ÙØªÙ‡ + label_user_activity: "گزارش %{value}" + label_new: تازه + label_logged_as: "نام کاربری:" + label_environment: محیط + label_authentication: شناسایی + label_auth_source: روش شناسایی + label_auth_source_new: روش شناسایی تازه + label_auth_source_plural: روش شناسایی + label_subproject_plural: زیرپروژه + label_subproject_new: زیرپروژه تازه + label_and_its_subprojects: "%{value} Ùˆ زیرپروژه‌هایش" + label_min_max_length: کمترین Ùˆ بیشترین اندازه + label_list: Ùهرست + label_date: تاریخ + label_integer: شماره درست + label_float: شماره شناور + label_boolean: درست/نادرست + label_string: نوشته + label_text: نوشته بلند + label_attribute: نشانه + label_attribute_plural: نشانه + label_no_data: هیچ داده‌ای برای نمایش نیست + label_change_status: جایگزینی وضعیت + label_history: پیشینه + label_attachment: پرونده + label_attachment_new: پرونده تازه + label_attachment_delete: پاک کردن پرونده + label_attachment_plural: پرونده + label_file_added: پرونده Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_report: گزارش + label_report_plural: گزارش + label_news: رویداد + label_news_new: Ø§ÙØ²ÙˆØ¯Ù† رویداد + label_news_plural: رویداد + label_news_latest: آخرین رویدادها + label_news_view_all: دیدن همه رویدادها + label_news_added: رویداد Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_settings: پیکربندی + label_overview: در یک نگاه + label_version: نگارش + label_version_new: نگارش تازه + label_version_plural: نگارش + label_close_versions: بستن نگارش‌های انجام شده + label_confirmation: بررسی + label_export_to: 'قالب‌های دیگر:' + label_read: خواندن... + label_public_projects: پروژه‌های همگانی + label_open_issues: باز + label_open_issues_plural: باز + label_closed_issues: بسته + label_closed_issues_plural: بسته + label_x_open_issues_abbr_on_total: + zero: 0 باز از %{total} + one: 1 باز از %{total} + other: "%{count} باز از %{total}" + label_x_open_issues_abbr: + zero: 0 باز + one: 1 باز + other: "%{count} باز" + label_x_closed_issues_abbr: + zero: 0 بسته + one: 1 بسته + other: "%{count} بسته" + label_total: جمله + label_permissions: پروانه‌ها + label_current_status: وضعیت کنونی + label_new_statuses_allowed: وضعیت‌های Ù¾Ø°ÛŒØ±ÙØªÙ†ÛŒ تازه + label_all: همه + label_none: هیچ + label_nobody: هیچکس + label_next: پسین + label_previous: پیشین + label_used_by: به کار Ø±ÙØªÙ‡ در + label_details: ریزه‌کاری + label_add_note: Ø§ÙØ²ÙˆØ¯Ù† یادداشت + label_per_page: ردیÙ‌ها در هر برگه + label_calendar: گاهشمار + label_months_from: از ماه + label_gantt: گانت + label_internal: درونی + label_last_changes: "%{count} تغییر آخر" + label_change_view_all: دیدن همه تغییرات + label_personalize_page: Ø³ÙØ§Ø±Ø´ÛŒ نمودن این برگه + label_comment: دیدگاه + label_comment_plural: دیدگاه + label_x_comments: + zero: بدون دیدگاه + one: 1 دیدگاه + other: "%{count} دیدگاه" + label_comment_add: Ø§ÙØ²ÙˆØ¯Ù† دیدگاه + label_comment_added: دیدگاه Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_comment_delete: پاک کردن دیدگاه‌ها + label_query: پرس‌وجوی Ø³ÙØ§Ø±Ø´ÛŒ + label_query_plural: پرس‌وجوی Ø³ÙØ§Ø±Ø´ÛŒ + label_query_new: پرس‌وجوی تازه + label_filter_add: Ø§ÙØ²ÙˆØ¯Ù† پالایه + label_filter_plural: پالایه + label_equals: برابر است با + label_not_equals: برابر نیست با + label_in_less_than: کمتر است از + label_in_more_than: بیشتر است از + label_greater_or_equal: بیشتر یا برابر است با + label_less_or_equal: کمتر یا برابر است با + label_in: در + label_today: امروز + label_all_time: همیشه + label_yesterday: دیروز + label_this_week: این Ù‡ÙØªÙ‡ + label_last_week: Ù‡ÙØªÙ‡ پیشین + label_last_n_days: "%{count} روز گذشته" + label_this_month: این ماه + label_last_month: ماه پیشین + label_this_year: امسال + label_date_range: بازه تاریخ + label_less_than_ago: کمتر از چند روز پیشین + label_more_than_ago: بیشتر از چند روز پیشین + label_ago: روز پیشین + label_contains: دارد + label_not_contains: ندارد + label_day_plural: روز + label_repository: انباره + label_repository_plural: انباره + label_browse: چریدن + label_branch: شاخه + label_tag: برچسب + label_revision: بازبینی + label_revision_plural: بازبینی + label_revision_id: "بازبینی %{value}" + label_associated_revisions: بازبینی‌های وابسته + label_added: Ø§ÙØ²ÙˆØ¯Ù‡ شده + label_modified: پیراسته شده + label_copied: رونویسی شده + label_renamed: نامگذاری شده + label_deleted: پاکسازی شده + label_latest_revision: آخرین بازبینی + label_latest_revision_plural: آخرین بازبینی + label_view_revisions: دیدن بازبینی‌ها + label_view_all_revisions: دیدن همه بازبینی‌ها + label_max_size: بیشترین اندازه + label_sort_highest: بردن به آغاز + label_sort_higher: بردن به بالا + label_sort_lower: بردن به پایین + label_sort_lowest: بردن به پایان + label_roadmap: چشم‌انداز + label_roadmap_due_in: "سررسید در %{value}" + label_roadmap_overdue: "%{value} دیرکرد" + label_roadmap_no_issues: هیچ پیامدی برای این نگارش نیست + label_search: جستجو + label_result_plural: دست‌آورد + label_all_words: همه واژه‌ها + label_wiki: ویکی + label_wiki_edit: ویرایش ویکی + label_wiki_edit_plural: ویرایش ویکی + label_wiki_page: برگه ویکی + label_wiki_page_plural: برگه ویکی + label_index_by_title: شاخص بر اساس نام + label_index_by_date: شاخص بر اساس تاریخ + label_current_version: نگارش کنونی + label_preview: پیش‌نمایش + label_feed_plural: خوراک + label_changes_details: ریز همه جایگذاری‌ها + label_issue_tracking: پیگیری پیامد + label_spent_time: زمان گذاشته شده + label_overall_spent_time: زمان گذاشته شده روی هم + label_f_hour: "%{value} ساعت" + label_f_hour_plural: "%{value} ساعت" + label_time_tracking: پیگیری زمان + label_change_plural: جایگذاری + label_statistics: سرشماری + label_commits_per_month: تغییر در هر ماه + label_commits_per_author: تغییر هر نویسنده + label_view_diff: دیدن ØªÙØ§ÙˆØªâ€ŒÙ‡Ø§ + label_diff_inline: همراستا + label_diff_side_by_side: کنار به کنار + label_options: گزینه‌ها + label_copy_workflow_from: رونویسی گردش کار از روی + label_permissions_report: گزارش پروانه‌ها + label_watched_issues: پیامدهای دیده‌بانی شده + label_related_issues: پیامدهای وابسته + label_applied_status: وضعیت به کار Ø±ÙØªÙ‡ + label_loading: بار گذاری... + label_relation_new: وابستگی تازه + label_relation_delete: پاک کردن وابستگی + label_relates_to: وابسته به + label_duplicates: نگارش دیگری از + label_duplicated_by: نگارشی دیگر در + label_blocks: بازداشت‌ها + label_blocked_by: بازداشت به دست + label_precedes: جلوتر است از + label_follows: پستر است از + label_end_to_start: پایان به آغاز + label_end_to_end: پایان به پایان + label_start_to_start: آغاز به آغاز + label_start_to_end: آغاز به پایان + label_stay_logged_in: وارد شده بمانید + label_disabled: ØºÛŒØ±ÙØ¹Ø§Ù„ + label_show_completed_versions: نمایش نگارش‌های انجام شده + label_me: من + label_board: انجمن + label_board_new: انجمن تازه + label_board_plural: انجمن + label_board_locked: Ù‚ÙÙ„ شده + label_board_sticky: چسبناک + label_topic_plural: Ø³Ø±ÙØµÙ„ + label_message_plural: پیام + label_message_last: آخرین پیام + label_message_new: پیام تازه + label_message_posted: پیام Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_reply_plural: پاسخ + label_send_information: ÙØ±Ø³ØªØ§Ø¯Ù† داده‌های حساب به کاربر + label_year: سال + label_month: ماه + label_week: Ù‡ÙØªÙ‡ + label_date_from: از + label_date_to: تا + label_language_based: بر اساس زبان کاربر + label_sort_by: "جور کرد با %{value}" + label_send_test_email: ÙØ±Ø³ØªØ§Ø¯Ù† ایمیل آزمایشی + label_feeds_access_key: کلید دسترسی RSS + label_missing_feeds_access_key: کلید دسترسی RSS در دسترس نیست + label_feeds_access_key_created_on: "کلید دسترسی RSS %{value} پیش ساخته شده است" + label_module_plural: پیمانه + label_added_time_by: "Ø§ÙØ²ÙˆØ¯Ù‡ شده به دست %{author} در %{age} پیش" + label_updated_time_by: "بروز شده به دست %{author} در %{age} پیش" + label_updated_time: "بروز شده در %{value} پیش" + label_jump_to_a_project: پرش به یک پروژه... + label_file_plural: پرونده + label_changeset_plural: تغییر + label_default_columns: ستون‌های پیش‌گزیده + label_no_change_option: (بدون تغییر) + label_bulk_edit_selected_issues: ویرایش دسته‌ای پیامدهای گزینش شده + label_theme: پوسته + label_default: پیش‌گزیده + label_search_titles_only: تنها نام‌ها جستجو شود + label_user_mail_option_all: "برای هر رویداد در همه پروژه‌ها" + label_user_mail_option_selected: "برای هر رویداد تنها در پروژه‌های گزینش شده..." + label_user_mail_option_none: "هیچ رویدادی" + label_user_mail_option_only_my_events: "تنها برای چیزهایی Ú©Ù‡ دیده‌بان هستم یا در آن‌ها درگیر هستم" + label_user_mail_option_only_assigned: "تنها برای چیزهایی Ú©Ù‡ به من واگذار شده" + label_user_mail_option_only_owner: "تنها برای چیزهایی Ú©Ù‡ من دارنده آن‌ها هستم" + label_user_mail_no_self_notified: "نمی‌خواهم از تغییراتی Ú©Ù‡ خودم می‌دهم آگاه شوم" + label_registration_activation_by_email: ÙØ¹Ø§Ù„سازی حساب با ایمیل + label_registration_manual_activation: ÙØ¹Ø§Ù„سازی حساب دستی + label_registration_automatic_activation: ÙØ¹Ø§Ù„سازی حساب خودکار + label_display_per_page: "ردیÙ‌ها در هر برگه: %{value}" + label_age: سن + label_change_properties: ویرایش ویژگی‌ها + label_general: همگانی + label_more: بیشتر + label_scm: SCM + label_plugins: Ø§ÙØ²ÙˆÙ†Ù‡â€ŒÙ‡Ø§ + label_ldap_authentication: شناساییLDAP + label_downloads_abbr: Ø¯Ø±ÛŒØ§ÙØª + label_optional_description: یادداشت دلخواه + label_add_another_file: Ø§ÙØ²ÙˆØ¯Ù† پرونده دیگر + label_preferences: پسندها + label_chronological_order: به ترتیب تاریخ + label_reverse_chronological_order: برعکس ترتیب تاریخ + label_planning: برنامه ریزی + label_incoming_emails: ایمیل‌های آمده + label_generate_key: ساخت کلید + label_issue_watchers: دیده‌بان‌ها + label_example: نمونه + label_display: نمایش + label_sort: جور کرد + label_ascending: Ø§ÙØ²Ø§ÛŒØ´ÛŒ + label_descending: کاهشی + label_date_from_to: از %{start} تا %{end} + label_wiki_content_added: برگه ویکی Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_wiki_content_updated: برگه ویکی بروز شد + label_group: دسته + label_group_plural: دسته + label_group_new: دسته تازه + label_time_entry_plural: زمان گذاشته شده + label_version_sharing_none: بدون اشتراک + label_version_sharing_descendants: با زیر پروژه‌ها + label_version_sharing_hierarchy: با رشته پروژه‌ها + label_version_sharing_tree: با درخت پروژه + label_version_sharing_system: با همه پروژه‌ها + label_update_issue_done_ratios: بروز رسانی اندازه انجام شده پیامد + label_copy_source: منبع + label_copy_target: مقصد + label_copy_same_as_target: مانند مقصد + label_display_used_statuses_only: تنها وضعیت‌هایی نشان داده شوند Ú©Ù‡ در این پیگرد به کار Ø±ÙØªÙ‡â€ŒØ§Ù†Ø¯ + label_api_access_key: کلید دسترسی API + label_missing_api_access_key: کلید دسترسی API در دسترس نیست + label_api_access_key_created_on: "کلید دسترسی API %{value} پیش ساخته شده است" + label_profile: نمایه + label_subtask_plural: زیرکار + label_project_copy_notifications: در هنگام رونویسی پروژه ایمیل‌های آگاه‌سازی را Ø¨ÙØ±Ø³Øª + label_principal_search: "جستجو برای کاربر یا دسته:" + label_user_search: "جستجو برای کاربر:" + + button_login: ورود + button_submit: واگذاری + button_save: نگهداری + button_check_all: گزینش همه + button_uncheck_all: گزینش هیچ + button_delete: پاک + button_create: ساخت + button_create_and_continue: ساخت Ùˆ ادامه + button_test: آزمایش + button_edit: ویرایش + button_edit_associated_wikipage: "ویرایش برگه ویکی وابسته: %{page_title}" + button_add: Ø§ÙØ²ÙˆØ¯Ù† + button_change: ویرایش + button_apply: انجام + button_clear: پاک + button_lock: گذاشتن Ù‚ÙÙ„ + button_unlock: برداشتن Ù‚ÙÙ„ + button_download: Ø¯Ø±ÛŒØ§ÙØª + button_list: Ùهرست + button_view: دیدن + button_move: جابجایی + button_move_and_follow: جابجایی Ùˆ ادامه + button_back: برگشت + button_cancel: بازگشت + button_activate: ÙØ¹Ø§Ù„سازی + button_sort: جور کرد + button_log_time: زمان‌نویسی + button_rollback: برگرد به این نگارش + button_watch: دیده‌بانی + button_unwatch: نا‌دیده‌بانی + button_reply: پاسخ + button_archive: بایگانی + button_unarchive: برگشت از بایگانی + button_reset: بازنشانی + button_rename: نامگذاری + button_change_password: جایگزینی گذرواژه + button_copy: رونوشت + button_copy_and_follow: رونوشت Ùˆ ادامه + button_annotate: یادداشت + button_update: بروز رسانی + button_configure: پیکربندی + button_quote: نقل قول + button_duplicate: نگارش دیگر + button_show: نمایش + + status_active: ÙØ¹Ø§Ù„ + status_registered: نام‌نویسی شده + status_locked: Ù‚ÙÙ„ + + version_status_open: باز + version_status_locked: Ù‚ÙÙ„ + version_status_closed: بسته + + field_active: ÙØ¹Ø§Ù„ + + text_select_mail_notifications: ÙØ±Ù…ان‌هایی Ú©Ù‡ برای آن‌ها باید ایمیل ÙØ±Ø³ØªØ§Ø¯Ù‡ شود را برگزینید. + text_regexp_info: برای نمونه ^[A-Z0-9]+$ + text_min_max_length_info: 0 یعنی بدون کران + text_project_destroy_confirmation: آیا براستی می‌خواهید این پروژه Ùˆ همه داده‌های آن را پاک کنید؟ + text_subprojects_destroy_warning: "زیرپروژه‌های آن: %{value} هم پاک خواهند شد." + text_workflow_edit: یک نقش Ùˆ یک پیگرد را برای ویرایش گردش کار برگزینید + text_are_you_sure: آیا این کار انجام شود؟ + text_journal_changed: "«%{label}» از «%{old}» به «%{new}» جایگزین شد" + text_journal_set_to: "«%{label}» به «%{value}» نشانده شد" + text_journal_deleted: "«%{label}» پاک شد (%{old})" + text_journal_added: "«%{label}»، «%{value}» را Ø§ÙØ²ÙˆØ¯" + text_tip_task_begin_day: روز آغاز پیامد + text_tip_task_end_day: روز پایان پیامد + text_tip_task_begin_end_day: روز آغاز Ùˆ پایان پیامد + text_caracters_maximum: "بیشترین اندازه %{count} است." + text_caracters_minimum: "کمترین اندازه %{count} است." + text_length_between: "باید میان %{min} Ùˆ %{max} نویسه باشد." + text_tracker_no_workflow: هیچ گردش کاری برای این پیگرد مشخص نشده است + text_unallowed_characters: نویسه‌های ناپسند + text_comma_separated: چند مقدار Ù¾Ø°ÛŒØ±ÙØªÙ†ÛŒ است (با «,» از هم جدا شوند). + text_line_separated: چند مقدار Ù¾Ø°ÛŒØ±ÙØªÙ†ÛŒ است (هر مقدار در یک خط). + text_issues_ref_in_commit_messages: نشانه روی Ùˆ بستن پیامدها در پیام‌های انباره + text_issue_added: "پیامد %{id} به دست %{author} گزارش شد." + text_issue_updated: "پیامد %{id} به دست %{author} بروز شد." + text_wiki_destroy_confirmation: آیا براستی می‌خواهید این ویکی Ùˆ همه محتوای آن را پاک کنید؟ + text_issue_category_destroy_question: "برخی پیامدها (%{count}) به این دسته واگذار شده‌اند. می‌خواهید Ú†Ù‡ کنید؟" + text_issue_category_destroy_assignments: پاک کردن واگذاری به دسته + text_issue_category_reassign_to: واگذاری دوباره پیامدها به این دسته + text_user_mail_option: "برای پروژه‌های گزینش نشده، تنها ایمیل‌هایی درباره چیزهایی Ú©Ù‡ دیده‌بان یا درگیر آن‌ها هستید Ø¯Ø±ÛŒØ§ÙØª خواهید کرد (مانند پیامدهایی Ú©Ù‡ نویسنده آن‌ها هستید یا به شما واگذار شده‌اند)." + text_no_configuration_data: "نقش‌ها، پیگردها، وضعیت‌های پیامد Ùˆ گردش کار هنوز پیکربندی نشده‌اند. \nبه سختی پیشنهاد می‌شود Ú©Ù‡ پیکربندی پیش‌گزیده را بار کنید. سپس می‌توانید آن را ویرایش کنید." + text_load_default_configuration: بارگذاری پیکربندی پیش‌گزیده + text_status_changed_by_changeset: "در تغییر %{value} بروز شده است." + text_time_logged_by_changeset: "در تغییر %{value} نوشته شده است." + text_issues_destroy_confirmation: 'آیا براستی می‌خواهید پیامدهای گزینش شده را پاک کنید؟' + text_select_project_modules: 'پیمانه‌هایی Ú©Ù‡ باید برای این پروژه ÙØ¹Ø§Ù„ شوند را برگزینید:' + text_default_administrator_account_changed: حساب سرپرستی پیش‌گزیده جایگزین شد + text_file_repository_writable: پوشه پیوست‌ها نوشتنی است + text_plugin_assets_writable: پوشه دارایی‌های Ø§ÙØ²ÙˆÙ†Ù‡â€ŒÙ‡Ø§ نوشتنی است + text_rmagick_available: RMagick در دسترس است (اختیاری) + text_destroy_time_entries_question: "%{hours} ساعت روی پیامدهایی Ú©Ù‡ می‌خواهید پاک کنید کار گزارش شده است. می‌خواهید Ú†Ù‡ کنید؟" + text_destroy_time_entries: ساعت‌های گزارش شده پاک شوند + text_assign_time_entries_to_project: ساعت‌های گزارش شده به پروژه واگذار شوند + text_reassign_time_entries: 'ساعت‌های گزارش شده به این پیامد واگذار شوند:' + text_user_wrote: "%{value} نوشت:" + text_enumeration_destroy_question: "%{count} داده به این برشمردنی وابسته شده‌اند." + text_enumeration_category_reassign_to: 'به این برشمردنی وابسته شوند:' + text_email_delivery_not_configured: "Ø¯Ø±ÛŒØ§ÙØª ایمیل پیکربندی نشده است Ùˆ آگاه‌سازی‌ها غیر ÙØ¹Ø§Ù„ هستند.\nکارگزار SMTP خود را در config/configuration.yml پیکربندی کنید Ùˆ برنامه را بازنشانی کنید تا ÙØ¹Ø§Ù„ شوند." + text_repository_usernames_mapping: "کاربر Redmine Ú©Ù‡ به هر نام کاربری پیام‌های انباره نگاشت می‌شود را برگزینید.\nکاربرانی Ú©Ù‡ نام کاربری یا ایمیل همسان دارند، خود به خود نگاشت می‌شوند." + text_diff_truncated: '... این ØªÙØ§ÙˆØª بریده شده چون بیشتر از بیشترین اندازه نمایش دادنی است.' + text_custom_field_possible_values_info: 'یک خط برای هر مقدار' + text_wiki_page_destroy_question: "این برگه %{descendants} زیربرگه دارد.می‌خواهید Ú†Ù‡ کنید؟" + text_wiki_page_nullify_children: "زیربرگه‌ها برگه ریشه شوند" + text_wiki_page_destroy_children: "زیربرگه‌ها Ùˆ زیربرگه‌های آن‌ها پاک شوند" + text_wiki_page_reassign_children: "زیربرگه‌ها به زیر این برگه پدر بروند" + text_own_membership_delete_confirmation: "شما دارید برخی یا همه پروانه‌های خود را برمی‌دارید Ùˆ شاید پس از این دیگر نتوانید این پروژه را ویرایش کنید.\nآیا می‌خواهید این کار را بکنید؟" + text_zoom_in: درشتنمایی + text_zoom_out: ریزنمایی + + default_role_manager: سرپرست + default_role_developer: برنامه‌نویس + default_role_reporter: گزارش‌دهنده + default_tracker_bug: ایراد + default_tracker_feature: ویژگی + default_tracker_support: پشتیبانی + default_issue_status_new: تازه + default_issue_status_in_progress: در گردش + default_issue_status_resolved: درست شده + default_issue_status_feedback: بازخورد + default_issue_status_closed: بسته + default_issue_status_rejected: برگشت خورده + default_doc_category_user: نوشتار کاربر + default_doc_category_tech: نوشتار ÙÙ†ÛŒ + default_priority_low: پایین + default_priority_normal: میانه + default_priority_high: بالا + default_priority_urgent: زود + default_priority_immediate: بیدرنگ + default_activity_design: طراحی + default_activity_development: ساخت + + enumeration_issue_priorities: برتری‌های پیامد + enumeration_doc_categories: دسته‌های نوشتار + enumeration_activities: کارها (پیگیری زمان) + enumeration_system_activity: کار سامانه + + text_tip_issue_begin_day: پیامد در این روز آغاز می‌شود + field_warn_on_leaving_unsaved: هنگام ترک برگه‌ای Ú©Ù‡ نوشته‌های آن نگهداری نشده، به من هشدار بده + text_tip_issue_begin_end_day: پیامد در این روز آغاز می‌شود Ùˆ پایان می‌پذیرد + text_tip_issue_end_day: پیامد در این روز پایان می‌پذیرد + text_warn_on_leaving_unsaved: این برگه دارای نوشته‌های نگهداری نشده است Ú©Ù‡ اگر آن را ترک کنید، از میان می‌روند. + label_my_queries: جستارهای Ø³ÙØ§Ø±Ø´ÛŒ من + text_journal_changed_no_detail: "%{label} بروز شد" + label_news_comment_added: دیدگاه به یک رویداد Ø§ÙØ²ÙˆØ¯Ù‡ شد + button_expand_all: باز کردن همه + button_collapse_all: بستن همه + label_additional_workflow_transitions_for_assignee: زمانی Ú©Ù‡ به کاربر واگذار شده، گذارهای بیشتر Ù¾Ø°ÛŒØ±ÙØªÙ‡ می‌شود + label_additional_workflow_transitions_for_author: زمانی Ú©Ù‡ کاربر نویسنده است، گذارهای بیشتر Ù¾Ø°ÛŒØ±ÙØªÙ‡ می‌شود + label_bulk_edit_selected_time_entries: ویرایش دسته‌ای زمان‌های گزارش شده گزینش شده + text_time_entries_destroy_confirmation: آیا می‌خواهید زمان‌های گزارش شده گزینش شده پاک شوند؟ + label_role_anonymous: ناشناس + label_role_non_member: غیر عضو + label_issue_note_added: یادداشت Ø§ÙØ²ÙˆØ¯Ù‡ شد + label_issue_status_updated: وضعیت بروز شد + label_issue_priority_updated: برتری بروز شد + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: کدگذاری پیام‌های انباره + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 پیامد + one: 1 پیامد + other: "%{count} پیامد" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
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 + 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. diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/05/0539624edaa056ab07935479230d09abf0794955.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/0539624edaa056ab07935479230d09abf0794955.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/05/05764e8ea551fb41de1ac455c66cb6f2907259b8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05764e8ea551fb41de1ac455c66cb6f2907259b8.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/05/05c2095e86e69b0ded063e085ccf41b6310d9ac0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05c2095e86e69b0ded063e085ccf41b6310d9ac0.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,3 @@ +<%= title l(:label_custom_field_plural) %> + +<%= render_tabs custom_fields_tabs %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/05/05d8cde4a4e9772f7dcbafb481eff20153ca4409.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05d8cde4a4e9772f7dcbafb481eff20153ca4409.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,98 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/05/05fb4e3ebed828cf639f80e5d1d6da6095d43000.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05fb4e3ebed828cf639f80e5d1d6da6095d43000.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,42 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ProjectEnumerationsController < ApplicationController + before_filter :find_project_by_project_id + before_filter :authorize + + def update + if request.put? && params[:enumerations] + Project.transaction do + params[:enumerations].each do |id, activity| + @project.update_or_create_time_entry_activity(id, activity) + end + end + flash[:notice] = l(:notice_successful_update) + end + + redirect_to settings_project_path(@project, :tab => 'activities') + end + + def destroy + @project.time_entry_activities.each do |time_entry_activity| + time_entry_activity.destroy(time_entry_activity.parent) + end + flash[:notice] = l(:notice_successful_update) + redirect_to settings_project_path(@project, :tab => 'activities') + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/06/0661103167f6b9ecf574c505f5bd6a17eadf893e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/0661103167f6b9ecf574c505f5bd6a17eadf893e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,703 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require "digest/sha1" + +class User < Principal + include Redmine::SafeAttributes + + # Different ways of displaying/sorting users + USER_FORMATS = { + :firstname_lastname => { + :string => '#{firstname} #{lastname}', + :order => %w(firstname lastname id), + :setting_order => 1 + }, + :firstname_lastinitial => { + :string => '#{firstname} #{lastname.to_s.chars.first}.', + :order => %w(firstname lastname id), + :setting_order => 2 + }, + :firstname => { + :string => '#{firstname}', + :order => %w(firstname id), + :setting_order => 3 + }, + :lastname_firstname => { + :string => '#{lastname} #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 4 + }, + :lastname_coma_firstname => { + :string => '#{lastname}, #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 5 + }, + :lastname => { + :string => '#{lastname}', + :order => %w(lastname id), + :setting_order => 6 + }, + :username => { + :string => '#{login}', + :order => %w(login id), + :setting_order => 7 + }, + } + + MAIL_NOTIFICATION_OPTIONS = [ + ['all', :label_user_mail_option_all], + ['selected', :label_user_mail_option_selected], + ['only_my_events', :label_user_mail_option_only_my_events], + ['only_assigned', :label_user_mail_option_only_assigned], + ['only_owner', :label_user_mail_option_only_owner], + ['none', :label_user_mail_option_none] + ] + + has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, + :after_remove => Proc.new {|user, group| group.user_removed(user)} + has_many :changesets, :dependent => :nullify + has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' + has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" + has_one :api_token, :class_name => 'Token', :conditions => "action='api'" + belongs_to :auth_source + + scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") } + scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } + + acts_as_customizable + + attr_accessor :password, :password_confirmation + attr_accessor :last_before_login_on + # Prevents unauthorized assignments + attr_protected :login, :admin, :password, :password_confirmation, :hashed_password + + LOGIN_LENGTH_LIMIT = 60 + MAIL_LENGTH_LIMIT = 60 + + validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } + validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false + validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false + # Login must contain letters, numbers, underscores only + validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i + validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT + validates_length_of :firstname, :lastname, :maximum => 30 + validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true + validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true + validates_confirmation_of :password, :allow_nil => true + validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true + validate :validate_password_length + + before_create :set_mail_notification + before_save :update_hashed_password + before_destroy :remove_references_before_destroy + + scope :in_group, lambda {|group| + group_id = group.is_a?(Group) ? group.id : group.to_i + where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) + } + scope :not_in_group, lambda {|group| + group_id = group.is_a?(Group) ? group.id : group.to_i + where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) + } + scope :sorted, lambda { order(*User.fields_for_order_statement)} + + def set_mail_notification + self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? + true + end + + def update_hashed_password + # update hashed_password if password was set + if self.password && self.auth_source_id.blank? + salt_password(password) + end + end + + alias :base_reload :reload + def reload(*args) + @name = nil + @projects_by_role = nil + @membership_by_project_id = nil + base_reload(*args) + end + + def mail=(arg) + write_attribute(:mail, arg.to_s.strip) + end + + def identity_url=(url) + if url.blank? + write_attribute(:identity_url, '') + else + begin + write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) + rescue OpenIdAuthentication::InvalidOpenId + # Invalid url, don't save + end + end + self.read_attribute(:identity_url) + end + + # Returns the user that matches provided login and password, or nil + def self.try_to_login(login, password) + login = login.to_s + password = password.to_s + + # Make sure no one can sign in with an empty login or password + return nil if login.empty? || password.empty? + user = find_by_login(login) + if user + # user is already in local database + return nil unless user.active? + return nil unless user.check_password?(password) + else + # user is not yet registered, try to authenticate with available sources + attrs = AuthSource.authenticate(login, password) + if attrs + user = new(attrs) + user.login = login + user.language = Setting.default_language + if user.save + user.reload + logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source + end + end + end + user.update_column(:last_login_on, Time.now) if user && !user.new_record? + user + rescue => text + raise text + end + + # Returns the user who matches the given autologin +key+ or nil + def self.try_to_autologin(key) + user = Token.find_active_user('autologin', key, Setting.autologin.to_i) + if user + user.update_column(:last_login_on, Time.now) + user + end + end + + def self.name_formatter(formatter = nil) + USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] + end + + # Returns an array of fields names than can be used to make an order statement for users + # according to how user names are displayed + # Examples: + # + # User.fields_for_order_statement => ['users.login', 'users.id'] + # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id'] + def self.fields_for_order_statement(table=nil) + table ||= table_name + name_formatter[:order].map {|field| "#{table}.#{field}"} + end + + # Return user's full name for display + def name(formatter = nil) + f = self.class.name_formatter(formatter) + if formatter + eval('"' + f[:string] + '"') + else + @name ||= eval('"' + f[:string] + '"') + end + end + + def active? + self.status == STATUS_ACTIVE + end + + def registered? + self.status == STATUS_REGISTERED + end + + def locked? + self.status == STATUS_LOCKED + end + + def activate + self.status = STATUS_ACTIVE + end + + def register + self.status = STATUS_REGISTERED + end + + def lock + self.status = STATUS_LOCKED + end + + def activate! + update_attribute(:status, STATUS_ACTIVE) + end + + def register! + update_attribute(:status, STATUS_REGISTERED) + end + + def lock! + update_attribute(:status, STATUS_LOCKED) + end + + # Returns true if +clear_password+ is the correct user's password, otherwise false + def check_password?(clear_password) + if auth_source_id.present? + auth_source.authenticate(self.login, clear_password) + else + User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password + end + end + + # Generates a random salt and computes hashed_password for +clear_password+ + # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) + def salt_password(clear_password) + self.salt = User.generate_salt + self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") + end + + # Does the backend storage allow this user to change their password? + def change_password_allowed? + return true if auth_source.nil? + return auth_source.allow_password_changes? + end + + # Generate and set a random password. Useful for automated user creation + # Based on Token#generate_token_value + # + def random_password + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + password = '' + 40.times { |i| password << chars[rand(chars.size-1)] } + self.password = password + self.password_confirmation = password + self + end + + def pref + self.preference ||= UserPreference.new(:user => self) + end + + def time_zone + @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) + end + + def wants_comments_in_reverse_order? + self.pref[:comments_sorting] == 'desc' + end + + # Return user's RSS key (a 40 chars long string), used to access feeds + def rss_key + if rss_token.nil? + create_rss_token(:action => 'feeds') + end + rss_token.value + end + + # Return user's API key (a 40 chars long string), used to access the API + def api_key + if api_token.nil? + create_api_token(:action => 'api') + end + api_token.value + end + + # Return an array of project ids for which the user has explicitly turned mail notifications on + def notified_projects_ids + @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) + end + + def notified_project_ids=(ids) + Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) + Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? + @notified_projects_ids = nil + notified_projects_ids + end + + def valid_notification_options + self.class.valid_notification_options(self) + end + + # Only users that belong to more than 1 project can select projects for which they are notified + def self.valid_notification_options(user=nil) + # Note that @user.membership.size would fail since AR ignores + # :include association option when doing a count + if user.nil? || user.memberships.length < 1 + MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} + else + MAIL_NOTIFICATION_OPTIONS + end + end + + # Find a user account by matching the exact login and then a case-insensitive + # version. Exact matches will be given priority. + def self.find_by_login(login) + if login.present? + login = login.to_s + # First look for an exact match + user = where(:login => login).all.detect {|u| u.login == login} + unless user + # Fail over to case-insensitive if none was found + user = where("LOWER(login) = ?", login.downcase).first + end + user + end + end + + def self.find_by_rss_key(key) + Token.find_active_user('feeds', key) + end + + def self.find_by_api_key(key) + Token.find_active_user('api', key) + end + + # Makes find_by_mail case-insensitive + def self.find_by_mail(mail) + where("LOWER(mail) = ?", mail.to_s.downcase).first + end + + # Returns true if the default admin account can no longer be used + def self.default_admin_account_changed? + !User.active.find_by_login("admin").try(:check_password?, "admin") + end + + def to_s + name + end + + CSS_CLASS_BY_STATUS = { + STATUS_ANONYMOUS => 'anon', + STATUS_ACTIVE => 'active', + STATUS_REGISTERED => 'registered', + STATUS_LOCKED => 'locked' + } + + def css_classes + "user #{CSS_CLASS_BY_STATUS[status]}" + end + + # Returns the current day according to user's time zone + def today + if time_zone.nil? + Date.today + else + Time.now.in_time_zone(time_zone).to_date + end + end + + # Returns the day of +time+ according to user's time zone + def time_to_date(time) + if time_zone.nil? + time.to_date + else + time.in_time_zone(time_zone).to_date + end + end + + def logged? + true + end + + def anonymous? + !logged? + end + + # Returns user's membership for the given project + # or nil if the user is not a member of project + def membership(project) + project_id = project.is_a?(Project) ? project.id : project + + @membership_by_project_id ||= Hash.new {|h, project_id| + h[project_id] = memberships.where(:project_id => project_id).first + } + @membership_by_project_id[project_id] + end + + # Return user's roles for project + def roles_for_project(project) + roles = [] + # No role on archived projects + return roles if project.nil? || project.archived? + if logged? + # Find project membership + membership = membership(project) + if membership + roles = membership.roles + else + @role_non_member ||= Role.non_member + roles << @role_non_member + end + else + @role_anonymous ||= Role.anonymous + roles << @role_anonymous + end + roles + end + + # Return true if the user is a member of project + def member_of?(project) + projects.to_a.include?(project) + end + + # Returns a hash of user's projects grouped by roles + def projects_by_role + return @projects_by_role if @projects_by_role + + @projects_by_role = Hash.new([]) + memberships.each do |membership| + if membership.project + membership.roles.each do |role| + @projects_by_role[role] = [] unless @projects_by_role.key?(role) + @projects_by_role[role] << membership.project + end + end + end + @projects_by_role.each do |role, projects| + projects.uniq! + end + + @projects_by_role + end + + # Returns true if user is arg or belongs to arg + def is_or_belongs_to?(arg) + if arg.is_a?(User) + self == arg + elsif arg.is_a?(Group) + arg.users.include?(self) + else + false + end + end + + # Return true if the user is allowed to do the specified action on a specific context + # Action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + # Context can be: + # * a project : returns true if user is allowed to do the specified action on this project + # * an array of projects : returns true if user is allowed on every project + # * nil with options[:global] set : check if user has at least one role allowed for this action, + # or falls back to Non Member / Anonymous permissions depending if the user is logged + def allowed_to?(action, context, options={}, &block) + if context && context.is_a?(Project) + return false unless context.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + roles = roles_for_project(context) + return false unless roles + roles.any? {|role| + (context.is_public? || role.member?) && + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + elsif context && context.is_a?(Array) + if context.empty? + false + else + # Authorize if user is authorized on every element of the array + context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&) + end + elsif options[:global] + # Admin users are always authorized + return true if admin? + + # authorize if user has at least one role that has this permission + roles = memberships.collect {|m| m.roles}.flatten.uniq + roles << (self.logged? ? Role.non_member : Role.anonymous) + roles.any? {|role| + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + else + false + end + end + + # Is the user allowed to do the specified action on any project? + # See allowed_to? for the actions and valid options. + def allowed_to_globally?(action, options, &block) + allowed_to?(action, nil, options.reverse_merge(:global => true), &block) + end + + # Returns true if the user is allowed to delete his own account + def own_account_deletable? + Setting.unsubscribe? && + (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?) + end + + safe_attributes 'login', + 'firstname', + 'lastname', + 'mail', + 'mail_notification', + 'language', + 'custom_field_values', + 'custom_fields', + 'identity_url' + + safe_attributes 'status', + 'auth_source_id', + :if => lambda {|user, current_user| current_user.admin?} + + safe_attributes 'group_ids', + :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} + + # Utility method to help check if a user should be notified about an + # event. + # + # TODO: only supports Issue events currently + def notify_about?(object) + if mail_notification == 'all' + true + elsif mail_notification.blank? || mail_notification == 'none' + false + else + case object + when Issue + case mail_notification + when 'selected', 'only_my_events' + # user receives notifications for created/assigned issues on unselected projects + object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) + when 'only_assigned' + is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) + when 'only_owner' + object.author == self + end + when News + # always send to project members except when mail_notification is set to 'none' + true + end + end + end + + def self.current=(user) + Thread.current[:current_user] = user + end + + def self.current + Thread.current[:current_user] ||= User.anonymous + end + + # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only + # one anonymous user per database. + def self.anonymous + anonymous_user = AnonymousUser.first + if anonymous_user.nil? + anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) + raise 'Unable to create the anonymous user.' if anonymous_user.new_record? + end + anonymous_user + end + + # Salts all existing unsalted passwords + # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) + # This method is used in the SaltPasswords migration and is to be kept as is + def self.salt_unsalted_passwords! + transaction do + User.where("salt IS NULL OR salt = ''").find_each do |user| + next if user.hashed_password.blank? + salt = User.generate_salt + hashed_password = User.hash_password("#{salt}#{user.hashed_password}") + User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password) + end + end + end + + protected + + def validate_password_length + # Password length validation based on setting + if !password.nil? && password.size < Setting.password_min_length.to_i + errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) + end + end + + private + + # Removes references that are not handled by associations + # Things that are not deleted are reassociated with the anonymous user + def remove_references_before_destroy + return if self.id.nil? + + substitute = User.anonymous + Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] + Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] + JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] + Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + # Remove private queries and keep public ones + ::Query.delete_all ['user_id = ? AND is_public = ?', id, false] + ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + Token.delete_all ['user_id = ?', id] + Watcher.delete_all ['user_id = ?', id] + WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + end + + # Return password digest + def self.hash_password(clear_password) + Digest::SHA1.hexdigest(clear_password || "") + end + + # Returns a 128bits random salt as a hex string (32 chars long) + def self.generate_salt + Redmine::Utils.random_hex(16) + end + +end + +class AnonymousUser < User + validate :validate_anonymous_uniqueness, :on => :create + + def validate_anonymous_uniqueness + # There should be only one AnonymousUser in the database + errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists? + end + + def available_custom_fields + [] + end + + # Overrides a few properties + def logged?; false end + def admin; false end + def name(*args); I18n.t(:label_user_anonymous) end + def mail; nil end + def time_zone; nil end + def rss_key; nil end + + def pref + UserPreference.new(:user => self) + end + + def member_of?(project) + false + end + + # Anonymous user can not be destroyed + def destroy + false + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/06/06906d5d51f832720062a81972796df1a81f69a3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06906d5d51f832720062a81972796df1a81f69a3.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,167 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AdminControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_select 'div.nodata', 0 + end + + def test_index_with_no_configuration_data + delete_configuration_data + get :index + assert_select 'div.nodata' + end + + def test_projects + get :projects + assert_response :success + assert_template 'projects' + assert_not_nil assigns(:projects) + # active projects only + assert_nil assigns(:projects).detect {|u| !u.active?} + end + + def test_projects_with_status_filter + get :projects, :status => 1 + assert_response :success + assert_template 'projects' + assert_not_nil assigns(:projects) + # active projects only + assert_nil assigns(:projects).detect {|u| !u.active?} + end + + def test_projects_with_name_filter + get :projects, :name => 'store', :status => '' + assert_response :success + assert_template 'projects' + projects = assigns(:projects) + assert_not_nil projects + assert_equal 1, projects.size + assert_equal 'OnlineStore', projects.first.name + end + + def test_load_default_configuration_data + delete_configuration_data + post :default_configuration, :lang => 'fr' + assert_response :redirect + assert_nil flash[:error] + assert IssueStatus.find_by_name('Nouveau') + end + + def test_load_default_configuration_data_should_rescue_error + delete_configuration_data + Redmine::DefaultData::Loader.stubs(:load).raises(Exception.new("Something went wrong")) + post :default_configuration, :lang => 'fr' + assert_response :redirect + assert_not_nil flash[:error] + assert_match /Something went wrong/, flash[:error] + end + + def test_test_email + user = User.find(1) + user.pref.no_self_notified = '1' + user.pref.save! + ActionMailer::Base.deliveries.clear + + get :test_email + assert_redirected_to '/settings?tab=notifications' + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + user = User.find(1) + assert_equal [user.mail], mail.bcc + end + + def test_test_email_failure_should_display_the_error + Mailer.stubs(:test_email).raises(Exception, 'Some error message') + get :test_email + assert_redirected_to '/settings?tab=notifications' + assert_match /Some error message/, flash[:error] + end + + def test_no_plugins + Redmine::Plugin.clear + + get :plugins + assert_response :success + assert_template 'plugins' + end + + def test_plugins + # Register a few plugins + Redmine::Plugin.register :foo do + name 'Foo plugin' + author 'John Smith' + description 'This is a test plugin' + version '0.0.1' + settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings' + end + Redmine::Plugin.register :bar do + end + + get :plugins + assert_response :success + assert_template 'plugins' + + assert_select 'tr#plugin-foo' do + assert_select 'td span.name', :text => 'Foo plugin' + assert_select 'td.configure a[href=/settings/plugin/foo]' + end + assert_select 'tr#plugin-bar' do + assert_select 'td span.name', :text => 'Bar' + assert_select 'td.configure a', 0 + end + end + + def test_info + get :info + assert_response :success + assert_template 'info' + end + + def test_admin_menu_plugin_extension + Redmine::MenuManager.map :admin_menu do |menu| + menu.push :test_admin_menu_plugin_extension, '/foo/bar', :caption => 'Test' + end + + get :index + assert_response :success + assert_select 'div#admin-menu a[href=/foo/bar]', :text => 'Test' + + Redmine::MenuManager.map :admin_menu do |menu| + menu.delete :test_admin_menu_plugin_extension + end + end + + private + + def delete_configuration_data + Role.delete_all('builtin = 0') + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/06/06927559eed548b89dcc19d7fe7d3d26046b88c1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06927559eed548b89dcc19d7fe7d3d26046b88c1.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,428 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssuesHelper + include ApplicationHelper + + def issue_list(issues, &block) + ancestors = [] + issues.each do |issue| + while (ancestors.any? && !issue.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield issue, ancestors.size + ancestors << issue unless issue.leaf? + end + end + + # Renders a HTML/CSS tooltip + # + # To use, a trigger div is needed. This is a div with the class of "tooltip" + # that contains this method wrapped in a span with the class of "tip" + # + #
<%= link_to_issue(issue) %> + # <%= render_issue_tooltip(issue) %> + #
+ # + def render_issue_tooltip(issue) + @cached_label_status ||= l(:field_status) + @cached_label_start_date ||= l(:field_start_date) + @cached_label_due_date ||= l(:field_due_date) + @cached_label_assigned_to ||= l(:field_assigned_to) + @cached_label_priority ||= l(:field_priority) + @cached_label_project ||= l(:field_project) + + link_to_issue(issue) + "

".html_safe + + "#{@cached_label_project}: #{link_to_project(issue.project)}
".html_safe + + "#{@cached_label_status}: #{h(issue.status.name)}
".html_safe + + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
".html_safe + + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
".html_safe + + "#{@cached_label_assigned_to}: #{h(issue.assigned_to)}
".html_safe + + "#{@cached_label_priority}: #{h(issue.priority.name)}".html_safe + end + + def issue_heading(issue) + h("#{issue.tracker} ##{issue.id}") + end + + def render_issue_subject_with_tree(issue) + s = '' + ancestors = issue.root? ? [] : issue.ancestors.visible.all + ancestors.each do |ancestor| + s << '
' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id))) + end + s << '
' + subject = h(issue.subject) + if issue.is_private? + subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject + end + s << content_tag('h3', subject) + s << '
' * (ancestors.size + 1) + s.html_safe + end + + def render_descendants_tree(issue) + s = '
' + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + css = "issue issue-#{child.id} hascontextmenu" + css << " idnt idnt-#{level}" if level > 0 + s << content_tag('tr', + content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') + + content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') + + content_tag('td', h(child.status)) + + content_tag('td', link_to_user(child.assigned_to)) + + content_tag('td', progress_bar(child.done_ratio, :width => '80px')), + :class => css) + end + s << '
' + s.html_safe + end + + # Returns an array of error messages for bulk edited issues + def bulk_edit_error_messages(issues) + messages = {} + issues.each do |issue| + issue.errors.full_messages.each do |message| + messages[message] ||= [] + messages[message] << issue + end + end + messages.map { |message, issues| + "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ') + } + end + + # Returns a link for adding a new subtask to the given issue + def link_to_new_subtask(issue) + attrs = { + :tracker_id => issue.tracker, + :parent_issue_id => issue + } + link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs)) + end + + class IssueFieldsRows + include ActionView::Helpers::TagHelper + + def initialize + @left = [] + @right = [] + end + + def left(*args) + args.any? ? @left << cells(*args) : @left + end + + def right(*args) + args.any? ? @right << cells(*args) : @right + end + + def size + @left.size > @right.size ? @left.size : @right.size + end + + def to_html + html = ''.html_safe + blank = content_tag('th', '') + content_tag('td', '') + size.times do |i| + left = @left[i] || blank + right = @right[i] || blank + html << content_tag('tr', left + right) + end + html + end + + def cells(label, text, options={}) + content_tag('th', "#{label}:", options) + content_tag('td', text, options) + end + end + + def issue_fields_rows + r = IssueFieldsRows.new + yield r + r.to_html + end + + def render_custom_fields_rows(issue) + values = issue.visible_custom_field_values + return if values.empty? + ordered_values = [] + half = (values.size / 2.0).ceil + half.times do |i| + ordered_values << values[i] + ordered_values << values[i + half] + end + s = "\n" + n = 0 + ordered_values.compact.each do |value| + s << "\n\n" if n > 0 && (n % 2) == 0 + s << "\t#{ h(value.custom_field.name) }:#{ simple_format_without_paragraph(h(show_value(value))) }\n" + n += 1 + end + s << "\n" + s.html_safe + end + + def issues_destroy_confirmation_message(issues) + issues = [issues] unless issues.is_a?(Array) + message = l(:text_issues_destroy_confirmation) + descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2} + if descendant_count > 0 + issues.each do |issue| + next if issue.root? + issues.each do |other_issue| + descendant_count -= 1 if issue.is_descendant_of?(other_issue) + end + end + if descendant_count > 0 + message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count) + end + end + message + end + + def sidebar_queries + unless @sidebar_queries + @sidebar_queries = IssueQuery.visible. + order("#{Query.table_name}.name ASC"). + # Project specific queries and global queries + where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]). + all + end + @sidebar_queries + end + + def query_links(title, queries) + return '' if queries.empty? + # links to #index on issues/show + url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params + + content_tag('h3', title) + "\n" + + content_tag('ul', + queries.collect {|query| + css = 'query' + css << ' selected' if query == @query + content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css)) + }.join("\n").html_safe, + :class => 'queries' + ) + "\n" + end + + def render_sidebar_queries + out = ''.html_safe + out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?)) + out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?)) + out + end + + def email_issue_attributes(issue, user) + items = [] + %w(author status priority assigned_to category fixed_version).each do |attribute| + unless issue.disabled_core_fields.include?(attribute+"_id") + items << "#{l("field_#{attribute}")}: #{issue.send attribute}" + end + end + issue.visible_custom_field_values(user).each do |value| + items << "#{value.custom_field.name}: #{show_value(value)}" + end + items + end + + def render_email_issue_attributes(issue, user, html=false) + items = email_issue_attributes(issue, user) + if html + content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe) + else + items.map{|s| "* #{s}"}.join("\n") + end + end + + # Returns the textual representation of a journal details + # as an array of strings + def details_to_strings(details, no_html=false, options={}) + options[:only_path] = (options[:only_path] == false ? false : true) + strings = [] + values_by_field = {} + details.each do |detail| + if detail.property == 'cf' + field = detail.custom_field + if field && field.multiple? + values_by_field[field] ||= {:added => [], :deleted => []} + if detail.old_value + values_by_field[field][:deleted] << detail.old_value + end + if detail.value + values_by_field[field][:added] << detail.value + end + next + end + end + strings << show_detail(detail, no_html, options) + end + values_by_field.each do |field, changes| + detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s) + detail.instance_variable_set "@custom_field", field + if changes[:added].any? + detail.value = changes[:added] + strings << show_detail(detail, no_html, options) + elsif changes[:deleted].any? + detail.old_value = changes[:deleted] + strings << show_detail(detail, no_html, options) + end + end + strings + end + + # Returns the textual representation of a single journal detail + def show_detail(detail, no_html=false, options={}) + multiple = false + case detail.property + when 'attr' + field = detail.prop_key.to_s.gsub(/\_id$/, "") + label = l(("field_" + field).to_sym) + case detail.prop_key + when 'due_date', 'start_date' + value = format_date(detail.value.to_date) if detail.value + old_value = format_date(detail.old_value.to_date) if detail.old_value + + when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id', + 'priority_id', 'category_id', 'fixed_version_id' + value = find_name_by_reflection(field, detail.value) + old_value = find_name_by_reflection(field, detail.old_value) + + when 'estimated_hours' + value = "%0.02f" % detail.value.to_f unless detail.value.blank? + old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? + + when 'parent_id' + label = l(:field_parent_issue) + value = "##{detail.value}" unless detail.value.blank? + old_value = "##{detail.old_value}" unless detail.old_value.blank? + + when 'is_private' + value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank? + old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank? + end + when 'cf' + custom_field = detail.custom_field + if custom_field + multiple = custom_field.multiple? + label = custom_field.name + value = format_value(detail.value, custom_field.field_format) if detail.value + old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value + end + when 'attachment' + label = l(:label_attachment) + when 'relation' + if detail.value && !detail.old_value + rel_issue = Issue.visible.find_by_id(detail.value) + value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" : + (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path])) + elsif detail.old_value && !detail.value + rel_issue = Issue.visible.find_by_id(detail.old_value) + old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" : + (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path])) + end + label = l(detail.prop_key.to_sym) + end + call_hook(:helper_issues_show_detail_after_setting, + {:detail => detail, :label => label, :value => value, :old_value => old_value }) + + label ||= detail.prop_key + value ||= detail.value + old_value ||= detail.old_value + + unless no_html + label = content_tag('strong', label) + old_value = content_tag("i", h(old_value)) if detail.old_value + if detail.old_value && detail.value.blank? && detail.property != 'relation' + old_value = content_tag("del", old_value) + end + if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key) + # Link to the attachment if it has not been removed + value = link_to_attachment(atta, :download => true, :only_path => options[:only_path]) + if options[:only_path] != false && atta.is_text? + value += link_to( + image_tag('magnifier.png'), + :controller => 'attachments', :action => 'show', + :id => atta, :filename => atta.filename + ) + end + else + value = content_tag("i", h(value)) if value + end + end + + if detail.property == 'attr' && detail.prop_key == 'description' + s = l(:text_journal_changed_no_detail, :label => label) + unless no_html + diff_link = link_to 'diff', + {:controller => 'journals', :action => 'diff', :id => detail.journal_id, + :detail_id => detail.id, :only_path => options[:only_path]}, + :title => l(:label_view_diff) + s << " (#{ diff_link })" + end + s.html_safe + elsif detail.value.present? + case detail.property + when 'attr', 'cf' + if detail.old_value.present? + l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe + elsif multiple + l(:text_journal_added, :label => label, :value => value).html_safe + else + l(:text_journal_set_to, :label => label, :value => value).html_safe + end + when 'attachment', 'relation' + l(:text_journal_added, :label => label, :value => value).html_safe + end + else + l(:text_journal_deleted, :label => label, :old => old_value).html_safe + end + end + + # Find the name of an associated record stored in the field attribute + def find_name_by_reflection(field, id) + unless id.present? + return nil + end + association = Issue.reflect_on_association(field.to_sym) + if association + record = association.class_name.constantize.find_by_id(id) + if record + record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding) + return record.name + end + end + end + + # Renders issue children recursively + def render_api_issue_children(issue, api) + return if issue.leaf? + api.array :children do + issue.children.each do |child| + api.issue(:id => child.id) do + api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil? + api.subject child.subject + render_api_issue_children(child, api) + end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/06/06efa9c81bf4ffa47e3211023e46fcb09d4eac0f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06efa9c81bf4ffa47e3211023e46fcb09d4eac0f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,216 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RolesControllerTest < ActionController::TestCase + fixtures :roles, :users, :members, :member_roles, :workflows, :trackers + + def setup + 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.order('builtin, position').all, 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.order('builtin, position').all, 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/071c1af1d5f365dd709aaf606fafd0332e258d64.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/071c1af1d5f365dd709aaf606fafd0332e258d64.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,476 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine #: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 + 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? + + # 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 directory + File.join(self.class.directory, id.to_s) + 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/073a34bd97c7ca9066ab7d9fa3f008e0e0da8c60.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/073a34bd97c7ca9066ab7d9fa3f008e0e0da8c60.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,172 @@ +var draw_gantt = null; +var draw_top; +var draw_right; +var draw_left; + +var rels_stroke_width = 2; + +function setDrawArea() { + draw_top = $("#gantt_draw_area").position().top; + draw_right = $("#gantt_draw_area").width(); + draw_left = $("#gantt_area").scrollLeft(); +} + +function getRelationsArray() { + var arr = new Array(); + $.each($('div.task_todo[data-rels]'), function(index_div, element) { + var element_id = $(element).attr("id"); + if (element_id != null) { + var issue_id = element_id.replace("task-todo-issue-", ""); + var data_rels = $(element).data("rels"); + for (rel_type_key in data_rels) { + $.each(data_rels[rel_type_key], function(index_issue, element_issue) { + arr.push({issue_from: issue_id, issue_to: element_issue, + rel_type: rel_type_key}); + }); + } + } + }); + return arr; +} + +function drawRelations() { + var arr = getRelationsArray(); + $.each(arr, function(index_issue, element_issue) { + var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]); + var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]); + if (issue_from.size() == 0 || issue_to.size() == 0) { + return; + } + var issue_height = issue_from.height(); + var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top; + var issue_from_right = issue_from.position().left + issue_from.width(); + var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top; + var issue_to_left = issue_to.position().left; + var color = issue_relation_type[element_issue["rel_type"]]["color"]; + var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"]; + var issue_from_right_rel = issue_from_right + landscape_margin; + var issue_to_left_rel = issue_to_left - landscape_margin; + draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top, + "L", issue_from_right_rel + draw_left, issue_from_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + if (issue_from_right_rel < issue_to_left_rel) { + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, + "L", issue_from_right_rel + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top, + "L", issue_to_left + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + } else { + var issue_middle_top = issue_to_top + + (issue_height * + ((issue_from_top > issue_to_top) ? 1 : -1)); + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top, + "L", issue_from_right_rel + draw_left, issue_middle_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top, + "L", issue_to_left_rel + draw_left, issue_middle_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top, + "L", issue_to_left_rel + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top, + "L", issue_to_left + draw_left, issue_to_top]) + .attr({stroke: color, + "stroke-width": rels_stroke_width + }); + } + draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top, + "l", -4 * rels_stroke_width, -2 * rels_stroke_width, + "l", 0, 4 * rels_stroke_width, "z"]) + .attr({stroke: "none", + fill: color, + "stroke-linecap": "butt", + "stroke-linejoin": "miter" + }); + }); +} + +function getProgressLinesArray() { + var arr = new Array(); + var today_left = $('#today_line').position().left; + arr.push({left: today_left, top: 0}); + $.each($('div.issue-subject, div.version-name'), function(index, element) { + var t = $(element).position().top - draw_top ; + var h = ($(element).height() / 9); + var element_top_upper = t - h; + var element_top_center = t + (h * 3); + var element_top_lower = t + (h * 8); + var issue_closed = $(element).children('span').hasClass('issue-closed'); + var version_closed = $(element).children('span').hasClass('version-closed'); + if (issue_closed || version_closed) { + arr.push({left: today_left, top: element_top_center}); + } else { + var issue_done = $("#task-done-" + $(element).attr("id")); + var is_behind_start = $(element).children('span').hasClass('behind-start-date'); + var is_over_end = $(element).children('span').hasClass('over-end-date'); + if (is_over_end) { + arr.push({left: draw_right, top: element_top_upper, is_right_edge: true}); + arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true}); + } else if (issue_done.size() > 0) { + var done_left = issue_done.first().position().left + + issue_done.first().width(); + arr.push({left: done_left, top: element_top_center}); + } else if (is_behind_start) { + arr.push({left: 0 , top: element_top_upper, is_left_edge: true}); + arr.push({left: 0 , top: element_top_lower, is_left_edge: true, none_stroke: true}); + } else { + var todo_left = today_left; + var issue_todo = $("#task-todo-" + $(element).attr("id")); + if (issue_todo.size() > 0){ + todo_left = issue_todo.first().position().left; + } + arr.push({left: Math.min(today_left, todo_left), top: element_top_center}); + } + } + }); + return arr; +} + +function drawGanttProgressLines() { + var arr = getProgressLinesArray(); + var color = $("#today_line") + .css("border-left-color"); + var i; + for(i = 1 ; i < arr.length ; i++) { + if (!("none_stroke" in arr[i]) && + (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) && + !("is_left_edge" in arr[i - 1] && "is_left_edge" in arr[i])) + ) { + var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left; + var x2 = (arr[i].left == 0) ? 0 : arr[i].left + draw_left; + draw_gantt.path(["M", x1, arr[i - 1].top, + "L", x2, arr[i].top]) + .attr({stroke: color, "stroke-width": 2}); + } + } +} + +function drawGanttHandler() { + var folder = document.getElementById('gantt_draw_area'); + if(draw_gantt != null) + draw_gantt.clear(); + else + draw_gantt = Raphael(folder); + setDrawArea(); + if ($("#draw_progress_line").attr('checked')) + drawGanttProgressLines(); + if ($("#draw_rels").attr('checked')) + drawRelations(); +} diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/0781c86b247e4cfc1df644f5ac2c5e1a6fb8ca1d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/0781c86b247e4cfc1df644f5ac2c5e1a6fb8ca1d.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,2254 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :groups_users, + :trackers, :projects_trackers, + :enabled_modules, + :versions, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, :journals, :journal_details, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries + + include Redmine::I18n + + def teardown + 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, + :subject => 'test_create', + :description => 'IssueTest#test_create', :estimated_hours => '1:30') + assert issue.save + issue.reload + assert_equal 1.5, issue.estimated_hours + end + + def test_create_minimal + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'test_create') + assert issue.save + assert issue.description.nil? + assert_nil issue.estimated_hours + end + + def test_start_date_format_should_be_validated + set_language_if_valid 'en' + ['2012', 'ABC', '2012-15-20'].each do |invalid_date| + issue = Issue.new(:start_date => invalid_date) + assert !issue.valid? + assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}" + end + end + + def test_due_date_format_should_be_validated + set_language_if_valid 'en' + ['2012', 'ABC', '2012-15-20'].each do |invalid_date| + issue = Issue.new(:due_date => invalid_date) + assert !issue.valid? + assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}" + end + end + + def test_due_date_lesser_than_start_date_should_not_validate + set_language_if_valid 'en' + issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02') + assert !issue.valid? + assert_include 'Due date must be greater than start date', issue.errors.full_messages + 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') + field.update_attribute(:is_required, true) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :subject => 'test_create', + :description => 'IssueTest#test_create_with_required_custom_field') + assert issue.available_custom_fields.include?(field) + # No value for the custom field + assert !issue.save + assert_equal ["Database can't be blank"], issue.errors.full_messages + # Blank value + issue.custom_field_values = { field.id => '' } + assert !issue.save + assert_equal ["Database can't be blank"], issue.errors.full_messages + # Invalid value + issue.custom_field_values = { field.id => 'SQLServer' } + assert !issue.save + assert_equal ["Database is not included in the list"], issue.errors.full_messages + # Valid value + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + issue.reload + assert_equal 'PostgreSQL', issue.custom_value_for(field).value + end + + def test_create_with_group_assignment + with_settings :issue_group_assignment => '1' do + 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') + assert_kind_of Group, issue.assigned_to + assert_equal Group.find(11), issue.assigned_to + end + end + + def test_create_with_parent_issue_id + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => 1) + assert_save issue + assert_equal 1, issue.parent_issue_id + assert_equal Issue.find(1), issue.parent + end + + def test_create_with_sharp_parent_issue_id + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => "#1") + assert_save issue + assert_equal 1, issue.parent_issue_id + assert_equal Issue.find(1), issue.parent + end + + def test_create_with_invalid_parent_issue_id + set_language_if_valid 'en' + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => '01ABC') + assert !issue.save + assert_equal '01ABC', issue.parent_issue_id + assert_include 'Parent task is invalid', issue.errors.full_messages + end + + def test_create_with_invalid_sharp_parent_issue_id + set_language_if_valid 'en' + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => '#01ABC') + assert !issue.save + assert_equal '#01ABC', issue.parent_issue_id + assert_include 'Parent task is invalid', issue.errors.full_messages + end + + def assert_visibility_match(user, issues) + assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort + end + + def test_visible_scope_for_anonymous + # Anonymous user should see issues of public projects only + issues = Issue.visible(User.anonymous).all + assert issues.any? + assert_nil issues.detect {|issue| !issue.project.is_public?} + assert_nil issues.detect {|issue| issue.is_private?} + assert_visibility_match User.anonymous, issues + end + + def test_visible_scope_for_anonymous_without_view_issues_permissions + # Anonymous user should not see issues without permission + Role.anonymous.remove_permission!(:view_issues) + issues = Issue.visible(User.anonymous).all + assert issues.empty? + assert_visibility_match User.anonymous, issues + end + + def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default + assert Role.anonymous.update_attribute(:issues_visibility, 'default') + issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true) + assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first + assert !issue.visible?(User.anonymous) + end + + def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own + assert Role.anonymous.update_attribute(:issues_visibility, 'own') + issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true) + assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first + assert !issue.visible?(User.anonymous) + end + + def test_visible_scope_for_non_member + user = User.find(9) + assert user.projects.empty? + # Non member user should see issues of public projects only + issues = Issue.visible(user).all + assert issues.any? + assert_nil issues.detect {|issue| !issue.project.is_public?} + assert_nil issues.detect {|issue| issue.is_private?} + assert_visibility_match user, issues + end + + def test_visible_scope_for_non_member_with_own_issues_visibility + Role.non_member.update_attribute :issues_visibility, 'own' + Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member') + user = User.find(9) + + issues = Issue.visible(user).all + assert issues.any? + assert_nil issues.detect {|issue| issue.author != user} + assert_visibility_match user, issues + end + + def test_visible_scope_for_non_member_without_view_issues_permissions + # Non member user should not see issues without permission + Role.non_member.remove_permission!(:view_issues) + user = User.find(9) + assert user.projects.empty? + issues = Issue.visible(user).all + assert issues.empty? + assert_visibility_match user, issues + end + + def test_visible_scope_for_member + user = User.find(9) + # User should see issues of projects for which he 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 + assert issues.any? + assert_nil issues.detect {|issue| issue.project_id != 3} + assert_nil issues.detect {|issue| issue.is_private?} + assert_visibility_match user, issues + end + + def test_visible_scope_for_member_with_groups_should_return_assigned_issues + user = User.find(8) + 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? + assert issues.include?(issue) + end + + def test_visible_scope_for_admin + user = User.find(1) + user.members.each(&:destroy) + 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 + 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} + assert_visibility_match user, issues + end + + def test_visible_scope_with_project + project = Project.find(1) + issues = Issue.visible(User.find(2), :project => project).all + projects = issues.collect(&:project).uniq + assert_equal 1, projects.size + assert_equal project, projects.first + end + + def test_visible_scope_with_project_and_subprojects + project = Project.find(1) + issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all + projects = issues.collect(&:project).uniq + assert projects.size > 1 + assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)} + end + + def test_visible_and_nested_set_scopes + assert_equal 0, Issue.find(1).descendants.visible.all.size + end + + def test_open_scope + issues = Issue.open.all + assert_nil issues.detect(&:closed?) + end + + def test_open_scope_with_arg + issues = Issue.open(false).all + 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') + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :subject => 'test_create', + :description => 'IssueTest#test_create_with_required_custom_field') + assert issue.available_custom_fields.include?(field) + # Invalid value + issue.custom_field_values = { field.id => 'SQLServer' } + + assert !issue.valid? + assert_equal 1, issue.errors.full_messages.size + assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", + issue.errors.full_messages.first + end + + def test_update_issue_with_required_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + issue = Issue.find(1) + assert_nil issue.custom_value_for(field) + assert issue.available_custom_fields.include?(field) + # No change to custom values, issue can be saved + assert issue.save + # Blank value + issue.custom_field_values = { field.id => '' } + assert !issue.save + # Valid value + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + issue.reload + assert_equal 'PostgreSQL', issue.custom_value_for(field).value + end + + def test_should_not_update_attributes_if_custom_fields_validation_fails + issue = Issue.find(1) + field = IssueCustomField.find_by_name('Database') + assert issue.available_custom_fields.include?(field) + + issue.custom_field_values = { field.id => 'Invalid' } + issue.subject = 'Should be not be saved' + assert !issue.save + + issue.reload + assert_equal "Can't print recipes", issue.subject + end + + def test_should_not_recreate_custom_values_objects_on_update + field = IssueCustomField.find_by_name('Database') + + issue = Issue.find(1) + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + custom_value = issue.custom_value_for(field) + issue.reload + issue.custom_field_values = { field.id => 'MySQL' } + assert issue.save + issue.reload + assert_equal custom_value.id, issue.custom_value_for(field).id + end + + def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields + issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :subject => 'Test', + :custom_field_values => {'2' => 'Test'}) + assert !Tracker.find(2).custom_field_ids.include?(2) + + issue = Issue.find(issue.id) + issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}} + + issue = Issue.find(issue.id) + custom_value = issue.custom_value_for(2) + assert_not_nil custom_value + assert_equal 'Test', custom_value.value + end + + def test_assigning_tracker_id_should_reload_custom_fields_values + issue = Issue.new(:project => Project.find(1)) + assert issue.custom_field_values.empty? + issue.tracker_id = 1 + assert issue.custom_field_values.any? + end + + def test_assigning_attributes_should_assign_project_and_tracker_first + seq = sequence('seq') + issue = Issue.new + issue.expects(:project_id=).in_sequence(seq) + issue.expects(:tracker_id=).in_sequence(seq) + issue.expects(:subject=).in_sequence(seq) + issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'} + end + + def test_assigning_tracker_and_custom_fields_should_assign_custom_fields + attributes = ActiveSupport::OrderedHash.new + attributes['custom_field_values'] = { '1' => 'MySQL' } + attributes['tracker_id'] = '1' + issue = Issue.new(:project => Project.find(1)) + issue.attributes = attributes + 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) + + p.trackers.delete(issue.tracker) + assert !p.trackers.include?(issue.tracker) + + issue.reload + issue.subject = 'New subject' + assert issue.save + end + + def test_should_not_set_a_disabled_tracker + p = Project.find(1) + p.trackers.delete(Tracker.find(2)) + + issue = Issue.find(1) + issue.tracker_id = 2 + issue.subject = 'New subject' + assert !issue.save + assert_not_equal [], issue.errors[:tracker_id] + end + + def test_category_based_assignment + issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'Assignment test', + :description => 'Assignment test', :category_id => 1) + assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to + end + + def test_new_statuses_allowed_to + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 2, + :author => false, :assignee => false) + 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 => 5, + :author => true, :assignee => true) + status = IssueStatus.find(1) + role = Role.find(1) + tracker = Tracker.find(1) + user = User.find(2) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author_id => 1) + assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author => user) + assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author_id => 1, + :assigned_to => user) + assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :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 + assert_equal expected_statuses, issue.new_statuses_allowed_to(admin) + end + + def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying + issue = Issue.find(1).copy + assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id) + + issue = Issue.find(2).copy + assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id) + end + + def test_safe_attributes_names_should_not_include_disabled_field + tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id)) + + issue = Issue.new(:tracker => tracker) + assert_include 'tracker_id', issue.safe_attribute_names + assert_include 'status_id', issue.safe_attribute_names + assert_include 'subject', issue.safe_attribute_names + assert_include 'description', issue.safe_attribute_names + assert_include 'custom_field_values', issue.safe_attribute_names + assert_include 'custom_fields', issue.safe_attribute_names + assert_include 'lock_version', issue.safe_attribute_names + + tracker.core_fields.each do |field| + assert_include field, issue.safe_attribute_names + end + + tracker.disabled_core_fields.each do |field| + assert_not_include field, issue.safe_attribute_names + end + end + + def test_safe_attributes_should_ignore_disabled_fields + tracker = Tracker.find(1) + tracker.core_fields = %w(assigned_to_id due_date) + tracker.save! + + issue = Issue.new(:tracker => tracker) + issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'} + assert_nil issue.start_date + assert_equal Date.parse('2012-07-14'), issue.due_date + end + + def test_safe_attributes_should_accept_target_tracker_enabled_fields + source = Tracker.find(1) + source.core_fields = [] + source.save! + target = Tracker.find(2) + target.core_fields = %w(assigned_to_id due_date) + target.save! + + issue = Issue.new(:tracker => source) + issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'} + assert_equal target, issue.tracker + assert_equal Date.parse('2012-07-14'), issue.due_date + end + + def test_safe_attributes_should_not_include_readonly_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + assert_equal %w(due_date), issue.read_only_attribute_names(user) + assert_not_include 'due_date', issue.safe_attribute_names(user) + + issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-14'), issue.start_date + assert_nil issue.due_date + end + + def test_safe_attributes_should_not_include_readonly_custom_fields + cf1 = IssueCustomField.create!(:name => 'Writable field', + :field_format => 'string', + :is_for_all => true, :tracker_ids => [1]) + cf2 = IssueCustomField.create!(:name => 'Readonly field', + :field_format => 'string', + :is_for_all => true, :tracker_ids => [1]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => cf2.id.to_s, + :rule => 'readonly') + user = User.find(2) + issue = Issue.new(:project_id => 1, :tracker_id => 1) + assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user) + assert_not_include cf2.id.to_s, issue.safe_attribute_names(user) + + issue.send :safe_attributes=, {'custom_field_values' => { + cf1.id.to_s => 'value1', cf2.id.to_s => 'value2' + }}, user + assert_equal 'value1', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + + issue.send :safe_attributes=, {'custom_fields' => [ + {'id' => cf1.id.to_s, 'value' => 'valuea'}, + {'id' => cf2.id.to_s, 'value' => 'valueb'} + ]}, user + assert_equal 'valuea', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + end + + def test_editable_custom_field_values_should_return_non_readonly_custom_values + cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', + :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Readonly field', :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 => cf2.id.to_s, :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + values = issue.editable_custom_field_values(user) + assert values.detect {|value| value.custom_field == cf1} + assert_nil values.detect {|value| value.custom_field == cf2} + + issue.tracker_id = 2 + values = issue.editable_custom_field_values(user) + assert values.detect {|value| value.custom_field == cf1} + assert values.detect {|value| value.custom_field == cf2} + end + + def test_safe_attributes_should_accept_target_tracker_writable_fields + 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 => 2, + :role_id => 1, :field_name => 'start_date', + :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + issue.send :safe_attributes=, {'start_date' => '2012-07-12', + 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_nil issue.due_date + + issue.send :safe_attributes=, {'start_date' => '2012-07-15', + 'due_date' => '2012-07-16', + 'tracker_id' => 2}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_equal Date.parse('2012-07-16'), issue.due_date + end + + def test_safe_attributes_should_accept_target_status_writable_fields + 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 => 2, :tracker_id => 1, + :role_id => 1, :field_name => 'start_date', + :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + issue.send :safe_attributes=, {'start_date' => '2012-07-12', + 'due_date' => '2012-07-14'}, + user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_nil issue.due_date + + issue.send :safe_attributes=, {'start_date' => '2012-07-15', + 'due_date' => '2012-07-16', + 'status_id' => 2}, + user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_equal Date.parse('2012-07-16'), issue.due_date + end + + def test_required_attributes_should_be_validated + cf = IssueCustomField.create!(:name => 'Foo', :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 => 'category_id', + :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => cf.id.to_s, + :rule => 'required') + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, + :role_id => 1, :field_name => 'start_date', + :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, + :role_id => 1, :field_name => cf.id.to_s, + :rule => 'required') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :status_id => 1, :subject => 'Required fields', + :author => user) + assert_equal [cf.id.to_s, "category_id", "due_date"], + issue.required_attribute_names(user).sort + assert !issue.save, "Issue was saved" + assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"], + issue.errors.full_messages.sort + + issue.tracker_id = 2 + assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort + assert !issue.save, "Issue was saved" + assert_equal ["Foo can't be blank", "Start date can't be blank"], + issue.errors.full_messages.sort + + issue.start_date = Date.today + issue.custom_field_values = {cf.id.to_s => 'bar'} + assert issue.save + end + + def test_required_attribute_names_for_multiple_roles_should_intersect_rules + 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 => 'start_date', + :rule => 'required') + user = User.find(2) + member = Member.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort + + member.role_ids = [1, 2] + member.save! + assert_equal [], issue.required_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 2, :field_name => 'due_date', + :rule => 'required') + assert_equal %w(due_date), issue.required_attribute_names(user) + + member.role_ids = [1, 2, 3] + member.save! + assert_equal [], issue.required_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 2, :field_name => 'due_date', + :rule => 'readonly') + # required + readonly => required + assert_equal %w(due_date), issue.required_attribute_names(user) + end + + def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules + 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 => 'start_date', + :rule => 'readonly') + user = User.find(2) + member = Member.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort + + member.role_ids = [1, 2] + member.save! + assert_equal [], issue.read_only_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 2, :field_name => 'due_date', + :rule => 'readonly') + assert_equal %w(due_date), issue.read_only_attribute_names(user) + end + + def test_copy + issue = Issue.new.copy_from(1) + assert issue.copy? + assert issue.save + issue.reload + orig = Issue.find(1) + assert_equal orig.subject, issue.subject + assert_equal orig.tracker, issue.tracker + assert_equal "125", issue.custom_value_for(2).value + end + + def test_copy_should_copy_status + orig = Issue.find(8) + assert orig.status != IssueStatus.default + + issue = Issue.new.copy_from(orig) + assert issue.save + issue.reload + assert_equal orig.status, issue.status + end + + def test_copy_should_add_relation_with_copied_issue + copied = Issue.find(1) + issue = Issue.new.copy_from(copied) + assert issue.save + issue.reload + + assert_equal 1, issue.relations.size + relation = issue.relations.first + assert_equal 'copied_to', relation.relation_type + assert_equal copied, relation.issue_from + assert_equal issue, relation.issue_to + end + + def test_copy_should_copy_subtasks + issue = Issue.generate_with_descendants! + + copy = issue.reload.copy + copy.author = User.find(7) + assert_difference 'Issue.count', 1+issue.descendants.count do + assert copy.save + end + copy.reload + assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort + child_copy = copy.children.detect {|c| c.subject == 'Child1'} + assert_equal %w(Child11), child_copy.children.map(&:subject).sort + 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! + + copy = issue.copy(:project_id => 3) + assert_difference 'Issue.count', 1+issue.descendants.count do + assert copy.save + end + assert_equal [3], copy.reload.descendants.map(&:project_id).uniq + end + + def test_copy_should_not_copy_subtasks_twice_when_saving_twice + issue = Issue.generate_with_descendants! + + copy = issue.reload.copy + assert_difference 'Issue.count', 1+issue.descendants.count do + assert copy.save + assert copy.save + end + end + + def test_should_not_call_after_project_change_on_creation + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, + :subject => 'Test', :author_id => 1) + issue.expects(:after_project_change).never + issue.save! + end + + def test_should_not_call_after_project_change_on_update + issue = Issue.find(1) + issue.project = Project.find(1) + issue.subject = 'No project change' + issue.expects(:after_project_change).never + issue.save! + end + + def test_should_call_after_project_change_on_project_change + issue = Issue.find(1) + issue.project = Project.find(2) + issue.expects(:after_project_change).once + issue.save! + end + + def test_adding_journal_should_update_timestamp + issue = Issue.find(1) + updated_on_was = issue.updated_on + + issue.init_journal(User.first, "Adding notes") + assert_difference 'Journal.count' do + assert issue.save + end + issue.reload + + assert_not_equal updated_on_was, issue.updated_on + end + + def test_should_close_duplicates + # Create 3 issues + issue1 = Issue.generate! + issue2 = Issue.generate! + issue3 = Issue.generate! + + # 2 is a dupe of 1 + IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 2 + IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 1 (circular duplicates) + IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_DUPLICATES) + + assert issue1.reload.duplicates.include?(issue2) + + # Closing issue 1 + 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? + assert issue3.reload.closed? + end + + def test_should_not_close_duplicated_issue + issue1 = Issue.generate! + issue2 = Issue.generate! + + # 2 is a dupe of 1 + IssueRelation.create(:issue_from => issue2, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_DUPLICATES) + # 2 is a dup of 1 but 1 is not a duplicate of 2 + assert !issue2.reload.duplicates.include?(issue1) + + # Closing issue 2 + 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? + end + + def test_assignable_versions + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 1, + :subject => 'New issue') + assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq + end + + def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 1, + :subject => 'New issue') + assert !issue.save + assert_not_equal [], issue.errors[:fixed_version_id] + end + + def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 2, + :subject => 'New issue') + assert !issue.save + assert_not_equal [], issue.errors[:fixed_version_id] + end + + def test_should_be_able_to_assign_a_new_issue_to_an_open_version + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 3, + :subject => 'New issue') + assert issue.save + end + + def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version + issue = Issue.find(11) + assert_equal 'closed', issue.fixed_version.status + issue.subject = 'Subject changed' + assert issue.save + end + + def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version + issue = Issue.find(11) + issue.status_id = 1 + assert !issue.save + assert_not_equal [], issue.errors[:base] + end + + def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version + issue = Issue.find(11) + issue.status_id = 1 + issue.fixed_version_id = 3 + assert issue.save + end + + def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version + issue = Issue.find(12) + assert_equal 'locked', issue.fixed_version.status + issue.status_id = 1 + assert issue.save + end + + def test_should_not_be_able_to_keep_unshared_version_when_changing_project + issue = Issue.find(2) + assert_equal 2, issue.fixed_version_id + issue.project_id = 3 + assert_nil issue.fixed_version_id + issue.fixed_version_id = 2 + assert !issue.save + assert_include 'Target version is not included in the list', issue.errors.full_messages + end + + 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 + assert_equal 2, issue.fixed_version_id + assert issue.save + end + + def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled + assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2)) + end + + def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled + Project.find(2).disable_module! :issue_tracking + assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2)) + end + + def test_move_to_another_project_with_same_category + issue = Issue.find(1) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Category changes + assert_equal 4, issue.category_id + # Make sure time entries were move to the target project + assert_equal 2, issue.time_entries.first.project_id + end + + def test_move_to_another_project_without_same_category + issue = Issue.find(2) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Category cleared + assert_nil issue.category_id + end + + def test_move_to_another_project_should_clear_fixed_version_when_not_shared + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 1) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Cleared fixed_version + assert_equal nil, issue.fixed_version + end + + def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 4) + issue.project = Project.find(5) + assert issue.save + issue.reload + assert_equal 5, issue.project_id + # Keep fixed_version + assert_equal 4, issue.fixed_version_id + end + + def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 1) + issue.project = Project.find(5) + assert issue.save + issue.reload + assert_equal 5, issue.project_id + # Cleared fixed_version + assert_equal nil, issue.fixed_version + end + + def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 7) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Keep fixed_version + assert_equal 7, issue.fixed_version_id + end + + def test_move_to_another_project_should_keep_parent_if_valid + issue = Issue.find(1) + issue.update_attribute(:parent_issue_id, 2) + issue.project = Project.find(3) + assert issue.save + issue.reload + assert_equal 2, issue.parent_id + end + + def test_move_to_another_project_should_clear_parent_if_not_valid + issue = Issue.find(1) + issue.update_attribute(:parent_issue_id, 2) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_nil issue.parent_id + end + + def test_move_to_another_project_with_disabled_tracker + issue = Issue.find(1) + target = Project.find(2) + target.tracker_ids = [3] + target.save + issue.project = target + assert issue.save + issue.reload + assert_equal 2, issue.project_id + assert_equal 3, issue.tracker_id + end + + def test_copy_to_the_same_project + issue = Issue.find(1) + copy = issue.copy + assert_difference 'Issue.count' do + copy.save! + end + assert_kind_of Issue, copy + assert_equal issue.project, copy.project + assert_equal "125", copy.custom_value_for(2).value + end + + def test_copy_to_another_project_and_tracker + issue = Issue.find(1) + copy = issue.copy(:project_id => 3, :tracker_id => 2) + assert_difference 'Issue.count' do + copy.save! + end + copy.reload + assert_kind_of Issue, copy + assert_equal Project.find(3), copy.project + assert_equal Tracker.find(2), copy.tracker + # Custom field #2 is not associated with target tracker + assert_nil copy.custom_value_for(2) + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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! + + 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 + issue = Issue.find(1) + issue_in_same_project = Issue.find(2) + issue_in_child_project = Issue.find(5) + issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1) + issue_in_other_child_project = Issue.find(6) + issue_in_different_tree = Issue.find(4) + + with_settings :cross_project_subtasks => '' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal false, issue.valid_parent_project?(issue_in_child_project) + assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project) + assert_equal false, issue.valid_parent_project?(issue_in_different_tree) + end + + with_settings :cross_project_subtasks => 'system' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal true, issue.valid_parent_project?(issue_in_child_project) + assert_equal true, issue.valid_parent_project?(issue_in_different_tree) + end + + with_settings :cross_project_subtasks => 'tree' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal true, issue.valid_parent_project?(issue_in_child_project) + assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project) + assert_equal false, issue.valid_parent_project?(issue_in_different_tree) + + assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project) + assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project) + end + + with_settings :cross_project_subtasks => 'descendants' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal false, issue.valid_parent_project?(issue_in_child_project) + assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project) + assert_equal false, issue.valid_parent_project?(issue_in_different_tree) + + assert_equal true, issue_in_child_project.valid_parent_project?(issue) + assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project) + end + end + + def test_recipients_should_include_previous_assignee + user = User.find(3) + user.members.update_all ["mail_notification = ?", false] + user.update_attribute :mail_notification, 'only_assigned' + + issue = Issue.find(2) + issue.assigned_to = nil + assert_include user.mail, issue.recipients + issue.save! + assert !issue.recipients.include?(user.mail) + end + + def test_recipients_should_not_include_users_that_cannot_view_the_issue + issue = Issue.find(12) + assert issue.recipients.include?(issue.author.mail) + # copy the issue to a private project + copy = issue.copy(:project_id => 5, :tracker_id => 2) + # author is not a member of project anymore + assert !copy.recipients.include?(copy.author.mail) + end + + def test_recipients_should_include_the_assigned_group_members + group_member = User.generate! + group = Group.generate! + group.users << group_member + + issue = Issue.find(12) + issue.assigned_to = group + assert issue.recipients.include?(group_member.mail) + end + + def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue + user = User.find(3) + issue = Issue.find(9) + Watcher.create!(:user => user, :watchable => issue) + assert issue.watched_by?(user) + assert !issue.watcher_recipients.include?(user.mail) + end + + def test_issue_destroy + Issue.find(1).destroy + assert_nil Issue.find_by_id(1) + assert_nil TimeEntry.find_by_issue_id(1) + end + + def test_destroying_a_deleted_issue_should_not_raise_an_error + issue = Issue.find(1) + Issue.find(1).destroy + + assert_nothing_raised do + assert_no_difference 'Issue.count' do + issue.destroy + end + assert issue.destroyed? + end + end + + def test_destroying_a_stale_issue_should_not_raise_an_error + issue = Issue.find(1) + Issue.find(1).update_attribute :subject, "Updated" + + assert_nothing_raised do + assert_difference 'Issue.count', -1 do + issue.destroy + end + assert issue.destroyed? + end + end + + def test_blocked + blocked_issue = Issue.find(9) + blocking_issue = Issue.find(10) + + assert blocked_issue.blocked? + assert !blocking_issue.blocked? + end + + def test_blocked_issues_dont_allow_closed_statuses + blocked_issue = Issue.find(9) + + allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002)) + assert !allowed_statuses.empty? + closed_statuses = allowed_statuses.select {|st| st.is_closed?} + assert closed_statuses.empty? + end + + def test_unblocked_issues_allow_closed_statuses + blocking_issue = Issue.find(10) + + allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002)) + assert !allowed_statuses.empty? + closed_statuses = allowed_statuses.select {|st| st.is_closed?} + assert !closed_statuses.empty? + end + + def test_reschedule_an_issue_without_dates + with_settings :non_working_week_days => [] do + issue = Issue.new(:start_date => nil, :due_date => nil) + issue.reschedule_on '2012-10-09'.to_date + assert_equal '2012-10-09'.to_date, issue.start_date + assert_equal '2012-10-09'.to_date, issue.due_date + end + + with_settings :non_working_week_days => %w(6 7) do + issue = Issue.new(:start_date => nil, :due_date => nil) + issue.reschedule_on '2012-10-09'.to_date + assert_equal '2012-10-09'.to_date, issue.start_date + assert_equal '2012-10-09'.to_date, issue.due_date + + issue = Issue.new(:start_date => nil, :due_date => nil) + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-15'.to_date, issue.start_date + assert_equal '2012-10-15'.to_date, issue.due_date + end + end + + def test_reschedule_an_issue_with_start_date + with_settings :non_working_week_days => [] do + issue = Issue.new(:start_date => '2012-10-09', :due_date => nil) + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-13'.to_date, issue.start_date + assert_equal '2012-10-13'.to_date, issue.due_date + end + + with_settings :non_working_week_days => %w(6 7) do + issue = Issue.new(:start_date => '2012-10-09', :due_date => nil) + issue.reschedule_on '2012-10-11'.to_date + assert_equal '2012-10-11'.to_date, issue.start_date + assert_equal '2012-10-11'.to_date, issue.due_date + + issue = Issue.new(:start_date => '2012-10-09', :due_date => nil) + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-15'.to_date, issue.start_date + assert_equal '2012-10-15'.to_date, issue.due_date + end + end + + def test_reschedule_an_issue_with_start_and_due_dates + with_settings :non_working_week_days => [] do + issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15') + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-13'.to_date, issue.start_date + assert_equal '2012-10-19'.to_date, issue.due_date + end + + with_settings :non_working_week_days => %w(6 7) do + issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days + issue.reschedule_on '2012-10-11'.to_date + assert_equal '2012-10-11'.to_date, issue.start_date + assert_equal '2012-10-23'.to_date, issue.due_date + + issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-15'.to_date, issue.start_date + assert_equal '2012-10-25'.to_date, issue.due_date + end + end + + def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + + issue1.due_date = '2012-10-23' + issue1.save! + issue2.reload + assert_equal Date.parse('2012-10-24'), issue2.start_date + assert_equal Date.parse('2012-10-26'), issue2.due_date + end + + def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + + issue1.start_date = '2012-09-17' + issue1.due_date = '2012-09-18' + issue1.save! + issue2.reload + assert_equal Date.parse('2012-09-19'), issue2.start_date + assert_equal Date.parse('2012-09-21'), issue2.due_date + end + + def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + + issue1.start_date = '2012-09-17' + issue1.due_date = '2012-09-18' + issue1.save! + issue2.reload + # Issue 2 must start after Issue 3 + assert_equal Date.parse('2012-10-03'), issue2.start_date + assert_equal Date.parse('2012-10-05'), issue2.due_date + end + + def test_rescheduling_a_stale_issue_should_not_raise_an_error + with_settings :non_working_week_days => [] do + stale = Issue.find(1) + issue = Issue.find(1) + issue.subject = "Updated" + issue.save! + date = 10.days.from_now.to_date + assert_nothing_raised do + stale.reschedule_on!(date) + end + assert_equal date, stale.reload.start_date + 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 is invalid', 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.where(:is_closed => true).first + ).overdue? + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + test "#assignable_users without issue_group_assignment 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 + + def test_create_should_send_email_notification + 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') + + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_stale_issue_should_not_send_email_notification + ActionMailer::Base.deliveries.clear + issue = Issue.find(1) + stale = Issue.find(1) + + issue.init_journal(User.find(1)) + issue.subject = 'Subjet update' + 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 + end + assert ActionMailer::Base.deliveries.empty? + end + + def test_journalized_description + IssueCustomField.delete_all + + i = Issue.first + old_description = i.description + new_description = "This is the new description" + + i.init_journal(User.find(2)) + i.description = new_description + assert_difference 'Journal.count', 1 do + assert_difference 'JournalDetail.count', 1 do + i.save! + end + end + + detail = JournalDetail.first(:order => 'id DESC') + assert_equal i, detail.journal.journalized + assert_equal 'attr', detail.property + assert_equal 'description', detail.prop_key + assert_equal old_description, detail.old_value + assert_equal new_description, detail.value + end + + def test_blank_descriptions_should_not_be_journalized + IssueCustomField.delete_all + Issue.update_all("description = NULL", "id=1") + + i = Issue.find(1) + i.init_journal(User.find(2)) + i.subject = "blank description" + i.description = "\r\n" + + assert_difference 'Journal.count', 1 do + assert_difference 'JournalDetail.count', 1 do + i.save! + end + end + end + + def test_journalized_multi_custom_field + field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', + :is_filter => true, :is_for_all => true, + :tracker_ids => [1], + :possible_values => ['value1', 'value2', 'value3'], + :multiple => true) + + issue = Issue.create!(:project_id => 1, :tracker_id => 1, + :subject => 'Test', :author_id => 1) + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => ['value1']} + issue.save! + end + assert_difference 'JournalDetail.count' do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => ['value1', 'value2']} + issue.save! + end + assert_difference 'JournalDetail.count', 2 do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => ['value3', 'value2']} + issue.save! + end + assert_difference 'JournalDetail.count', 2 do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => nil} + issue.save! + end + end + end + + def test_description_eol_should_be_normalized + i = Issue.new(:description => "CR \r LF \n CRLF \r\n") + assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description + end + + def test_saving_twice_should_not_duplicate_journal_details + i = Issue.first + i.init_journal(User.find(2), 'Some notes') + # initial changes + i.subject = 'New subject' + i.done_ratio = i.done_ratio + 10 + assert_difference 'Journal.count' do + assert i.save + end + # 1 more change + i.priority = IssuePriority.where("id <> ?", i.priority_id).first + assert_no_difference 'Journal.count' do + assert_difference 'JournalDetail.count', 1 do + i.save + end + end + # no more change + assert_no_difference 'Journal.count' do + assert_no_difference 'JournalDetail.count' do + i.save + end + end + end + + def test_all_dependent_issues + IssueRelation.delete_all + assert IssueRelation.create!(:issue_from => Issue.find(1), + :issue_to => Issue.find(2), + :relation_type => IssueRelation::TYPE_PRECEDES) + assert IssueRelation.create!(:issue_from => Issue.find(2), + :issue_to => Issue.find(3), + :relation_type => IssueRelation::TYPE_PRECEDES) + assert IssueRelation.create!(:issue_from => Issue.find(3), + :issue_to => Issue.find(8), + :relation_type => IssueRelation::TYPE_PRECEDES) + + 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), + :issue_to => Issue.find(2), + :relation_type => IssueRelation::TYPE_PRECEDES) + assert IssueRelation.create!(:issue_from => Issue.find(2), + :issue_to => Issue.find(3), + :relation_type => IssueRelation::TYPE_PRECEDES) + + 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]) + + assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort + end + + def test_all_dependent_issues_with_persistent_multiple_circular_dependencies + IssueRelation.delete_all + assert IssueRelation.create!(:issue_from => Issue.find(1), + :issue_to => Issue.find(2), + :relation_type => IssueRelation::TYPE_RELATES) + assert IssueRelation.create!(:issue_from => Issue.find(2), + :issue_to => Issue.find(3), + :relation_type => IssueRelation::TYPE_RELATES) + assert IssueRelation.create!(:issue_from => Issue.find(3), + :issue_to => Issue.find(8), + :relation_type => IssueRelation::TYPE_RELATES) + + 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]) + + 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]) + + assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort + end + + 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 + + with_settings :issue_done_ratio => 'issue_status' do + assert_equal 50, @issue.done_ratio + assert_equal 0, @issue2.done_ratio + end + end + + 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 + + with_settings :issue_done_ratio => 'issue_status' 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 + end + + test "#by_tracker" do + User.current = User.anonymous + groups = Issue.by_tracker(Project.find(1)) + assert_equal 3, groups.size + 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.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 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 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.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 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_subproject" do + User.current = User.anonymous + groups = Issue.by_subproject(Project.find(1)) + # Private descendant not visible + assert_equal 1, groups.size + assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + def test_recently_updated_scope + #should return the last updated issue + assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first + end + + def test_on_active_projects_scope + assert Project.find(2).archive + + before = Issue.on_active_project.length + # test inclusion to results + issue = Issue.generate!(:tracker => Project.find(2).trackers.first) + assert_equal before + 1, Issue.on_active_project.length + + # Move to an archived project + issue.project = Project.find(2) + assert issue.save + assert_equal before, Issue.on_active_project.length + end + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + assert_equal 2, Issue.find(1).last_journal_id + end + + def test_last_journal_id_without_journals_should_return_nil + assert_nil Issue.find(3).last_journal_id + end + + def test_journals_after_should_return_journals_with_greater_id + assert_equal [Journal.find(2)], Issue.find(1).journals_after('1') + assert_equal [], Issue.find(1).journals_after('2') + end + + def test_journals_after_with_blank_arg_should_return_all_journals + 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(' ') + assert_include 'priority-8', classes + assert_include 'priority-highest', classes + end + + def test_save_attachments_with_hash_should_save_attachments_in_keys_order + set_tmp_attachments_directory + issue = Issue.generate! + issue.save_attachments({ + 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')}, + '3' => {'file' => mock_file_with_options(:original_filename => 'bar')}, + '1' => {'file' => mock_file_with_options(:original_filename => 'foo')} + }) + issue.attach_saved_attachments + + 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/078a4373b77072983b303eb90be2e921eb7adcfd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/078a4373b77072983b303eb90be2e921eb7adcfd.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/07f4ae691a86068505e5dcb35963f143e7c54566.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/07f4ae691a86068505e5dcb35963f143e7c54566.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/07/07ff3aff4af2ca5266644ab7e838820cda8c8752.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/07ff3aff4af2ca5266644ab7e838820cda8c8752.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAutoCompletesTest < ActionController::IntegrationTest + def test_auto_completes + assert_routing( + { :method => 'get', :path => "/issues/auto_complete" }, + { :controller => 'auto_completes', :action => 'issues' } + ) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/086f4bbd984edf33971e918b1e5330fe445e9dd7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/086f4bbd984edf33971e918b1e5330fe445e9dd7.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,253 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class Redmine::MenuManager::MenuHelperTest < ActionView::TestCase + + include Redmine::MenuManager::MenuHelper + include ERB::Util + fixtures :users, :members, :projects, :enabled_modules, :roles, :member_roles + + def setup + setup_with_controller + # Stub the current menu item in the controller + def current_menu_item + :index + end + end + + context "MenuManager#current_menu_item" do + should "be tested" + end + + context "MenuManager#render_main_menu" do + should "be tested" + end + + context "MenuManager#render_menu" do + should "be tested" + end + + context "MenuManager#menu_item_and_children" do + should "be tested" + end + + context "MenuManager#extract_node_details" do + should "be tested" + end + + def test_render_single_menu_node + node = Redmine::MenuManager::MenuItem.new(:testing, '/test', { }) + @output_buffer = render_single_menu_node(node, 'This is a test', node.url, false) + + assert_select("a.testing", "This is a test") + end + + def test_render_menu_node + single_node = Redmine::MenuManager::MenuItem.new(:single_node, '/test', { }) + @output_buffer = render_menu_node(single_node, nil) + + assert_select("li") do + assert_select("a.single-node", "Single node") + end + end + + def test_render_menu_node_with_nested_items + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, '/test', { }) + parent_node << Redmine::MenuManager::MenuItem.new(:child_one_node, '/test', { }) + parent_node << Redmine::MenuManager::MenuItem.new(:child_two_node, '/test', { }) + parent_node << + Redmine::MenuManager::MenuItem.new(:child_three_node, '/test', { }) << + Redmine::MenuManager::MenuItem.new(:child_three_inner_node, '/test', { }) + + @output_buffer = render_menu_node(parent_node, nil) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.child-one-node", "Child one node") + assert_select("li a.child-two-node", "Child two node") + assert_select("li") do + assert_select("a.child-three-node", "Child three node") + assert_select("ul") do + assert_select("li a.child-three-inner-node", "Child three inner node") + end + end + end + end + + end + + def test_render_menu_node_with_children + User.current = User.find(2) + + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 3.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_child_#{time}", + {:controller => 'issues', :action => 'index'}, + {}) + end + children + } + }) + @output_buffer = render_menu_node(parent_node, Project.find(1)) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.test-child-0", "Test child 0") + assert_select("li a.test-child-1", "Test child 1") + assert_select("li a.test-child-2", "Test child 2") + end + end + end + + def test_render_menu_node_with_nested_items_and_children + User.current = User.find(2) + + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 3.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_child_#{time}", {:controller => 'issues', :action => 'index'}, {}) + end + children + } + }) + + parent_node << Redmine::MenuManager::MenuItem.new(:child_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 6.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_dynamic_child_#{time}", {:controller => 'issues', :action => 'index'}, {}) + end + children + } + }) + + @output_buffer = render_menu_node(parent_node, Project.find(1)) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.child-node", "Child node") + assert_select("ul") do + assert_select("li a.test-dynamic-child-0", "Test dynamic child 0") + assert_select("li a.test-dynamic-child-1", "Test dynamic child 1") + assert_select("li a.test-dynamic-child-2", "Test dynamic child 2") + assert_select("li a.test-dynamic-child-3", "Test dynamic child 3") + assert_select("li a.test-dynamic-child-4", "Test dynamic child 4") + assert_select("li a.test-dynamic-child-5", "Test dynamic child 5") + end + assert_select("li a.test-child-0", "Test child 0") + assert_select("li a.test-child-1", "Test child 1") + assert_select("li a.test-child-2", "Test child 2") + end + end + end + + def test_render_menu_node_with_children_without_an_array + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| Redmine::MenuManager::MenuItem.new("test_child", "/testing", {})} + }) + + assert_raises Redmine::MenuManager::MenuError, ":children must be an array of MenuItems" do + @output_buffer = render_menu_node(parent_node, Project.find(1)) + end + end + + def test_render_menu_node_with_incorrect_children + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| ["a string"] } + }) + + assert_raises Redmine::MenuManager::MenuError, ":children must be an array of MenuItems" do + @output_buffer = render_menu_node(parent_node, Project.find(1)) + end + + end + + def test_menu_items_for_should_yield_all_items_if_passed_a_block + menu_name = :test_menu_items_for_should_yield_all_items_if_passed_a_block + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, '/', { }) + menu.push(:a_menu_2, '/', { }) + menu.push(:a_menu_3, '/', { }) + end + + items_yielded = [] + menu_items_for(menu_name) do |item| + items_yielded << item + end + + assert_equal 3, items_yielded.size + end + + def test_menu_items_for_should_return_all_items + menu_name = :test_menu_items_for_should_return_all_items + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, '/', { }) + menu.push(:a_menu_2, '/', { }) + menu.push(:a_menu_3, '/', { }) + end + + items = menu_items_for(menu_name) + assert_equal 3, items.size + end + + def test_menu_items_for_should_skip_unallowed_items_on_a_project + menu_name = :test_menu_items_for_should_skip_unallowed_items_on_a_project + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:a_menu_2, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:unallowed, {:controller => 'issues', :action => 'unallowed' }, { }) + end + + User.current = User.find(2) + + items = menu_items_for(menu_name, Project.find(1)) + assert_equal 2, items.size + end + + def test_menu_items_for_should_skip_items_that_fail_the_conditions + menu_name = :test_menu_items_for_should_skip_items_that_fail_the_conditions + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:unallowed, + {:controller => 'issues', :action => 'index' }, + { :if => Proc.new { false }}) + end + + User.current = User.find(2) + + items = menu_items_for(menu_name, Project.find(1)) + assert_equal 1, items.size + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/088eae42f08379f3106075b388f036d1754d148c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/088eae42f08379f3106075b388f036d1754d148c.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,21 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module EnumerationsHelper +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/08918fc8f853142db140298c79057a45064d5050.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08918fc8f853142db140298c79057a45064d5050.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,72 @@ +desc "Run the Continous Integration tests for Redmine" +task :ci do + # RAILS_ENV and ENV[] can diverge so force them both to test + ENV['RAILS_ENV'] = 'test' + RAILS_ENV = 'test' + Rake::Task["ci:setup"].invoke + Rake::Task["ci:build"].invoke + Rake::Task["ci:teardown"].invoke +end + +namespace :ci do + desc "Setup Redmine for a new build" + task :setup do + Rake::Task["tmp:clear"].invoke + Rake::Task["log:clear"].invoke + Rake::Task["db:create:all"].invoke + Rake::Task["db:migrate"].invoke + Rake::Task["db:schema:dump"].invoke + Rake::Task["test:scm:setup:all"].invoke + Rake::Task["test:scm:update"].invoke + end + + desc "Build Redmine" + task :build do + Rake::Task["test"].invoke + # Rake::Task["test:ui"].invoke if RUBY_VERSION >= '1.9.3' + end + + desc "Finish the build" + task :teardown do + end +end + +desc "Creates database.yml for the CI server" +file 'config/database.yml' do + require 'yaml' + database = ENV['DATABASE_ADAPTER'] + ruby = ENV['RUBY_VER'].gsub('.', '').gsub('-', '') + branch = ENV['BRANCH'].gsub('.', '').gsub('-', '') + dev_db_name = "ci_#{branch}_#{ruby}_dev" + test_db_name = "ci_#{branch}_#{ruby}_test" + + case database + when 'mysql' + dev_conf = {'adapter' => (RUBY_VERSION >= '1.9' ? 'mysql2' : 'mysql'), + 'database' => dev_db_name, 'host' => 'localhost', + 'username' => 'jenkins', 'password' => 'jenkins', + 'encoding' => 'utf8'} + test_conf = dev_conf.merge('database' => test_db_name) + when 'postgresql' + dev_conf = {'adapter' => 'postgresql', 'database' => dev_db_name, + 'host' => 'localhost', + 'username' => 'jenkins', 'password' => 'jenkins'} + test_conf = dev_conf.merge('database' => test_db_name) + when /sqlite3/ + dev_conf = {'adapter' => (Object.const_defined?(:JRUBY_VERSION) ? + 'jdbcsqlite3' : 'sqlite3'), + 'database' => "db/#{dev_db_name}.sqlite3"} + test_conf = dev_conf.merge('database' => "db/#{test_db_name}.sqlite3") + when 'sqlserver' + dev_conf = {'adapter' => 'sqlserver', 'database' => dev_db_name, + 'host' => 'mssqlserver', 'port' => 1433, + 'username' => 'jenkins', 'password' => 'jenkins'} + test_conf = dev_conf.merge('database' => test_db_name) + else + abort "Unknown database" + end + + File.open('config/database.yml', 'w') do |f| + f.write YAML.dump({'development' => dev_conf, 'test' => test_conf}) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/08c1f1410c30c4d18ef6cbcbd8954d1721fb044b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08c1f1410c30c4d18ef6cbcbd8954d1721fb044b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,195 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class WatchersControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, + :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers + + def setup + User.current = nil + end + + def test_watch_a_single_object + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response :success + assert_include '$(".issue-1-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_watch_a_collection_with_a_single_object + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => ['1'] + assert_response :success + assert_include '$(".issue-1-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_watch_a_collection_with_multiple_objects + @request.session[:user_id] = 3 + assert_difference('Watcher.count', 2) do + xhr :post, :watch, :object_type => 'issue', :object_id => ['1', '3'] + assert_response :success + assert_include '$(".issue-bulk-watcher")', response.body + end + assert Issue.find(1).watched_by?(User.find(3)) + assert Issue.find(3).watched_by?(User.find(3)) + end + + def test_watch_should_be_denied_without_permission + Role.find(2).remove_permission! :view_issues + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response 403 + end + end + + def test_watch_invalid_class_should_respond_with_404 + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'foo', :object_id => '1' + assert_response 404 + end + end + + def test_watch_invalid_object_should_respond_with_404 + @request.session[:user_id] = 3 + assert_no_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '999' + assert_response 404 + end + end + + def test_unwatch + @request.session[:user_id] = 3 + assert_difference('Watcher.count', -1) do + xhr :delete, :unwatch, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_include '$(".issue-2-watcher")', response.body + end + assert !Issue.find(1).watched_by?(User.find(3)) + end + + def test_unwatch_a_collection_with_multiple_objects + @request.session[:user_id] = 3 + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + Watcher.create!(:user_id => 3, :watchable => Issue.find(3)) + + assert_difference('Watcher.count', -2) do + xhr :delete, :unwatch, :object_type => 'issue', :object_id => ['1', '3'] + assert_response :success + assert_include '$(".issue-bulk-watcher")', response.body + end + assert !Issue.find(1).watched_by?(User.find(3)) + assert !Issue.find(3).watched_by?(User.find(3)) + end + + def test_new + @request.session[:user_id] = 2 + xhr :get, :new, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_match /ajax-modal/, response.body + end + + def test_new_for_new_record_with_project_id + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => 1 + assert_response :success + assert_equal Project.find(1), assigns(:project) + assert_match /ajax-modal/, response.body + end + + def test_new_for_new_record_with_project_identifier + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => 'ecookbook' + assert_response :success + assert_equal Project.find(1), assigns(:project) + assert_match /ajax-modal/, response.body + end + + def test_create + @request.session[:user_id] = 2 + assert_difference('Watcher.count') do + xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(User.find(4)) + end + + def test_create_multiple + @request.session[:user_id] = 2 + assert_difference('Watcher.count', 2) do + xhr :post, :create, :object_type => 'issue', :object_id => '2', :watcher => {:user_ids => ['4', '7']} + assert_response :success + assert_match /watchers/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).watched_by?(User.find(4)) + assert Issue.find(2).watched_by?(User.find(7)) + end + + def test_autocomplete_on_watchable_creation + @request.session[:user_id] = 2 + xhr :get, :autocomplete_for_user, :q => 'mi', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 4 + assert_select 'input[name=?][value=1]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=2]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=8]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=9]', 'watcher[user_ids][]' + end + + def test_autocomplete_on_watchable_update + @request.session[:user_id] = 2 + xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue', :project_id => 'ecookbook' + assert_response :success + assert_select 'input', :count => 3 + assert_select 'input[name=?][value=2]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=8]', 'watcher[user_ids][]' + assert_select 'input[name=?][value=9]', 'watcher[user_ids][]' + + end + + def test_append + @request.session[:user_id] = 2 + assert_no_difference 'Watcher.count' do + xhr :post, :append, :watcher => {:user_ids => ['4', '7']}, :project_id => 'ecookbook' + assert_response :success + assert_include 'watchers_inputs', response.body + assert_include 'issue[watcher_user_ids][]', response.body + end + end + + def test_remove_watcher + @request.session[:user_id] = 2 + assert_difference('Watcher.count', -1) do + xhr :delete, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3' + assert_response :success + assert_match /watchers/, response.body + end + assert !Issue.find(2).watched_by?(User.find(3)) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/08/08d8c21c9156481395a983e5e0a4c9f73ad91ca9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08d8c21c9156481395a983e5e0a4c9f73ad91ca9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,71 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../../test_helper', __FILE__) + +begin + require 'mocha' + + class SubversionAdapterTest < ActiveSupport::TestCase + + if repository_configured?('subversion') + def setup + @adapter = Redmine::Scm::Adapters::SubversionAdapter.new(self.class.subversion_repository_url) + end + + def test_client_version + v = Redmine::Scm::Adapters::SubversionAdapter.client_version + assert v.is_a?(Array) + end + + def test_scm_version + to_test = { "svn, version 1.6.13 (r1002816)\n" => [1,6,13], + "svn, versione 1.6.13 (r1002816)\n" => [1,6,13], + "1.6.1\n1.7\n1.8" => [1,6,1], + "1.6.2\r\n1.8.1\r\n1.9.1" => [1,6,2]} + to_test.each do |s, v| + test_scm_version_for(s, v) + end + end + + def test_info_not_nil + assert_not_nil @adapter.info + end + + def test_info_nil + adpt = Redmine::Scm::Adapters::SubversionAdapter.new( + "file:///invalid/invalid/" + ) + assert_nil adpt.info + end + + private + + def test_scm_version_for(scm_version, version) + @adapter.class.expects(:scm_version_from_command_line).returns(scm_version) + assert_equal version, @adapter.class.svn_binary_version + end + else + puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end +rescue LoadError + class SubversionMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/094f0fabb02fa5ce798642abc8745a5aba89d2d9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/094f0fabb02fa5ce798642abc8745a5aba89d2d9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,175 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AccountControllerOpenidTest < ActionController::TestCase + tests AccountController + fixtures :users, :roles + + def setup + User.current = nil + Setting.openid = '1' + end + + def teardown + Setting.openid = '0' + end + + if Object.const_defined?(:OpenID) + + def test_login_with_openid_for_existing_user + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/my/page' + end + + def test_login_with_invalid_openid_provider + Setting.self_registration = '0' + post :login, :openid_url => 'http;//openid.example.com/good_user' + assert_redirected_to home_url + end + + def test_login_with_openid_for_existing_non_active_user + Setting.self_registration = '2' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user', + :status => User::STATUS_REGISTERED) + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/login' + end + + def test_login_with_openid_with_new_user_created + Setting.self_registration = '3' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/my/account' + user = User.find_by_login('cool_user') + assert user + assert_equal 'Cool', user.firstname + assert_equal 'User', user.lastname + end + + def test_login_with_openid_with_new_user_and_self_registration_off + Setting.self_registration = '0' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to home_url + user = User.find_by_login('cool_user') + assert_nil user + end + + def test_login_with_openid_with_new_user_created_with_email_activation_should_have_a_token + Setting.self_registration = '1' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + + token = Token.find_by_user_id_and_action(user.id, 'register') + assert token + end + + def test_login_with_openid_with_new_user_created_with_manual_activation + Setting.self_registration = '2' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + assert_equal User::STATUS_REGISTERED, user.status + end + + def test_login_with_openid_with_new_user_with_conflict_should_register + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', :lastname => 'User', :mail => 'user@somedomain.com') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_user', assigns(:user)[:identity_url] + end + + def test_login_with_openid_with_new_user_with_missing_information_should_register + Setting.self_registration = '3' + + post :login, :openid_url => 'http://openid.example.com/good_blank_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_blank_user', assigns(:user)[:identity_url] + + assert_select 'input[name=?]', 'user[login]' + assert_select 'input[name=?]', 'user[password]' + assert_select 'input[name=?]', 'user[password_confirmation]' + assert_select 'input[name=?][value=?]', 'user[identity_url]', 'http://openid.example.com/good_blank_user' + end + + def test_post_login_should_not_verify_token_when_using_open_id + ActionController::Base.allow_forgery_protection = true + AccountController.any_instance.stubs(:using_open_id?).returns(true) + AccountController.any_instance.stubs(:authenticate_with_open_id).returns(true) + post :login + assert_response 200 + ensure + ActionController::Base.allow_forgery_protection = false + end + + def test_register_after_login_failure_should_not_require_user_to_enter_a_password + Setting.self_registration = '3' + + assert_difference 'User.count' do + post :register, :user => { + :login => 'good_blank_user', + :password => '', + :password_confirmation => '', + :firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_blank_user' + } + assert_response 302 + end + + user = User.first(:order => 'id DESC') + assert_equal 'http://openid.example.com/good_blank_user', user.identity_url + assert user.hashed_password.blank?, "Hashed password was #{user.hashed_password}" + end + + def test_setting_openid_should_return_true_when_set_to_true + assert_equal true, Setting.openid? + end + + else + puts "Skipping openid tests." + + def test_dummy + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/09c2ce49203d7971d2ffb1539d86601fbcb17a3b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09c2ce49203d7971d2ffb1539d86601fbcb17a3b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,93 @@ +
+ <%= 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/09d005add00d07aeb449a6c04ad00db8e1f72fbd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09d005add00d07aeb449a6c04ad00db8e1f72fbd.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMyTest < ActionController::IntegrationTest + def test_my + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/account" }, + { :controller => 'my', :action => 'account' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/account/destroy" }, + { :controller => 'my', :action => 'destroy' } + ) + end + assert_routing( + { :method => 'get', :path => "/my/page" }, + { :controller => 'my', :action => 'page' } + ) + assert_routing( + { :method => 'get', :path => "/my" }, + { :controller => 'my', :action => 'index' } + ) + assert_routing( + { :method => 'post', :path => "/my/reset_rss_key" }, + { :controller => 'my', :action => 'reset_rss_key' } + ) + assert_routing( + { :method => 'post', :path => "/my/reset_api_key" }, + { :controller => 'my', :action => 'reset_api_key' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/my/password" }, + { :controller => 'my', :action => 'password' } + ) + end + assert_routing( + { :method => 'get', :path => "/my/page_layout" }, + { :controller => 'my', :action => 'page_layout' } + ) + assert_routing( + { :method => 'post', :path => "/my/add_block" }, + { :controller => 'my', :action => 'add_block' } + ) + assert_routing( + { :method => 'post', :path => "/my/remove_block" }, + { :controller => 'my', :action => 'remove_block' } + ) + assert_routing( + { :method => 'post', :path => "/my/order_blocks" }, + { :controller => 'my', :action => 'order_blocks' } + ) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/09/09e163f7a5ba48f41a7d0433361b8d9b46760493.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09e163f7a5ba48f41a7d0433361b8d9b46760493.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,55 @@ +<%= error_messages_for 'query' %> + +
+
+

+<%= text_field 'query', 'name', :size => 80 %>

+ +<% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %> +

+<%= check_box 'query', 'is_public', + :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %>

+<% end %> + +

+<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, + :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %>

+ +

+<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', + :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %>

+ +

+<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %>

+ +

+<%= available_block_columns_tags(@query) %>

+
+ +
<%= l(:label_filter_plural) %> +<%= render :partial => 'queries/filters', :locals => {:query => query}%> +
+ +
<%= l(:label_sort) %> +<% 3.times do |i| %> +<%= i+1 %>: +<%= label_tag "query_sort_criteria_attribute_" + i.to_s, + l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %> +<%= select_tag("query[sort_criteria][#{i}][]", + options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)), + :id => "query_sort_criteria_attribute_" + i.to_s)%> +<%= label_tag "query_sort_criteria_direction_" + i.to_s, + l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %> +<%= select_tag("query[sort_criteria][#{i}][]", + options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)), + :id => "query_sort_criteria_direction_" + i.to_s) %> +
+<% end %> +
+ +<%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %> +<%= l(:field_column_names) %> +<%= render_query_columns_selection(query) %> +<% end %> + +
diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0a/0a01ef88ba34bfc6a3c1cef8891538f7504e9b0c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0a01ef88ba34bfc6a3c1cef8891538f7504e9b0c.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,184 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module I18n + def self.included(base) + base.extend Redmine::I18n + end + + def l(*args) + case args.size + when 1 + ::I18n.t(*args) + when 2 + if args.last.is_a?(Hash) + ::I18n.t(*args) + elsif args.last.is_a?(String) + ::I18n.t(args.first, :value => args.last) + else + ::I18n.t(args.first, :count => args.last) + end + else + raise "Translation string with multiple values: #{args.first}" + end + end + + def l_or_humanize(s, options={}) + k = "#{options[:prefix]}#{s}".to_sym + ::I18n.t(k, :default => s.to_s.humanize) + end + + def l_hours(hours) + hours = hours.to_f + l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f)) + end + + def ll(lang, str, value=nil) + ::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" }) + end + + def format_date(date) + return nil unless date + options = {} + options[:format] = Setting.date_format unless Setting.date_format.blank? + options[:locale] = User.current.language unless User.current.language.blank? + ::I18n.l(date.to_date, options) + end + + def format_time(time, include_date = true) + return nil unless time + options = {} + options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format) + options[:locale] = User.current.language unless User.current.language.blank? + time = time.to_time if time.is_a?(String) + zone = User.current.time_zone + local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time) + (include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options) + end + + def day_name(day) + ::I18n.t('date.day_names')[day % 7] + end + + def day_letter(day) + ::I18n.t('date.abbr_day_names')[day % 7].first + end + + def month_name(month) + ::I18n.t('date.month_names')[month] + end + + def valid_languages + ::I18n.available_locales + end + + # Returns an array of languages names and code sorted by names, example: + # [["Deutsch", "de"], ["English", "en"] ...] + # + # The result is cached to prevent from loading all translations files. + def languages_options + ActionController::Base.cache_store.fetch "i18n/languages_options" do + valid_languages.map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.sort {|x,y| x.first <=> y.first } + end + end + + def find_language(lang) + @@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k } + @@languages_lookup[lang.to_s.downcase] + end + + def set_language_if_valid(lang) + if l = find_language(lang) + ::I18n.locale = l + end + end + + def current_language + ::I18n.locale + end + + # Custom backend based on I18n::Backend::Simple with the following changes: + # * lazy loading of translation files + # * available_locales are determined by looking at translation file names + class Backend + (class << self; self; end).class_eval { public :include } + + module Implementation + include ::I18n::Backend::Base + + # Stores translations for the given locale in memory. + # This uses a deep merge for the translations hash, so existing + # translations will be overwritten by new ones only at the deepest + # level of the hash. + def store_translations(locale, data, options = {}) + locale = locale.to_sym + translations[locale] ||= {} + data = data.deep_symbolize_keys + translations[locale].deep_merge!(data) + end + + # Get available locales from the translations filenames + def available_locales + @available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym) + end + + # Clean up translations + def reload! + @translations = nil + @available_locales = nil + super + end + + protected + + def init_translations(locale) + locale = locale.to_s + paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale} + load_translations(paths) + translations[locale] ||= {} + end + + def translations + @translations ||= {} + end + + # Looks up a translation from the translations hash. Returns nil if + # eiher key is nil, or locale, scope or key do not exist as a key in the + # nested translations hash. Splits keys or scopes containing dots + # into multiple keys, i.e. currency.format is regarded the same as + # %w(currency format). + def lookup(locale, key, scope = [], options = {}) + init_translations(locale) unless translations.key?(locale) + keys = ::I18n.normalize_keys(locale, key, scope, options[:separator]) + + keys.inject(translations) do |result, _key| + _key = _key.to_sym + return nil unless result.is_a?(Hash) && result.has_key?(_key) + result = result[_key] + result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol) + result + end + end + end + + include Implementation + # Adds fallback to default locale for untranslated strings + include ::I18n::Backend::Fallbacks + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0a/0ab473e135d07b597fe923abcf5c9582a5bc4830.svn-base Binary file .svn/pristine/0a/0ab473e135d07b597fe923abcf5c9582a5bc4830.svn-base has changed diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0a/0ab85a80b71d453bc34359193f9b3a077fde14bb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0ab85a80b71d453bc34359193f9b3a077fde14bb.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0b/0b7befce1d7322f245834ffc9480adf8d5c60890.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b7befce1d7322f245834ffc9480adf8d5c60890.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,294 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../test_case', __FILE__) +require 'tmpdir' + +class RedminePmTest::RepositorySubversionTest < RedminePmTest::TestCase + fixtures :projects, :users, :members, :roles, :member_roles, :auth_sources + + SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn" + + def test_anonymous_read_on_public_repo_with_permission_should_succeed + assert_success "ls", svn_url + end + + def test_anonymous_read_on_public_repo_without_permission_should_fail + Role.anonymous.remove_permission! :browse_repository + assert_failure "ls", svn_url + end + + def test_anonymous_read_on_private_repo_should_fail + Project.find(1).update_attribute :is_public, false + assert_failure "ls", svn_url + end + + def test_anonymous_commit_on_public_repo_should_fail + Role.anonymous.add_permission! :commit_access + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + + def test_anonymous_commit_on_private_repo_should_fail + Role.anonymous.add_permission! :commit_access + Project.find(1).update_attribute :is_public, false + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + + def test_non_member_read_on_public_repo_with_permission_should_succeed + Role.anonymous.remove_permission! :browse_repository + with_credentials "miscuser8", "foo" do + assert_success "ls", svn_url + end + end + + def test_non_member_read_on_public_repo_without_permission_should_fail + Role.anonymous.remove_permission! :browse_repository + Role.non_member.remove_permission! :browse_repository + with_credentials "miscuser8", "foo" do + assert_failure "ls", svn_url + end + end + + def test_non_member_read_on_private_repo_should_fail + Project.find(1).update_attribute :is_public, false + with_credentials "miscuser8", "foo" do + assert_failure "ls", svn_url + end + end + + def test_non_member_commit_on_public_repo_should_fail + Role.non_member.add_permission! :commit_access + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + + def test_non_member_commit_on_private_repo_should_fail + Role.non_member.add_permission! :commit_access + Project.find(1).update_attribute :is_public, false + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + + def test_member_read_on_public_repo_with_permission_should_succeed + Role.anonymous.remove_permission! :browse_repository + Role.non_member.remove_permission! :browse_repository + with_credentials "dlopper", "foo" do + assert_success "ls", svn_url + end + end + + def test_member_read_on_public_repo_without_permission_should_fail + Role.anonymous.remove_permission! :browse_repository + Role.non_member.remove_permission! :browse_repository + Role.find(2).remove_permission! :browse_repository + with_credentials "dlopper", "foo" do + assert_failure "ls", svn_url + end + end + + def test_member_read_on_private_repo_with_permission_should_succeed + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_success "ls", svn_url + end + end + + def test_member_read_on_private_repo_without_permission_should_fail + Role.find(2).remove_permission! :browse_repository + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_failure "ls", svn_url + end + end + + def test_member_commit_on_public_repo_with_permission_should_succeed + Role.find(2).add_permission! :commit_access + with_credentials "dlopper", "foo" do + assert_success "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + def test_member_commit_on_public_repo_without_permission_should_fail + Role.find(2).remove_permission! :commit_access + with_credentials "dlopper", "foo" do + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + def test_member_commit_on_private_repo_with_permission_should_succeed + Role.find(2).add_permission! :commit_access + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_success "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + def test_member_commit_on_private_repo_without_permission_should_fail + Role.find(2).remove_permission! :commit_access + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + def test_invalid_credentials_should_fail + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_success "ls", svn_url + end + with_credentials "dlopper", "wrong" do + assert_failure "ls", svn_url + end + end + + def test_anonymous_read_should_fail_with_login_required + assert_success "ls", svn_url + with_settings :login_required => '1' do + assert_failure "ls", svn_url + end + end + + def test_authenticated_read_should_succeed_with_login_required + with_settings :login_required => '1' do + with_credentials "miscuser8", "foo" do + assert_success "ls", svn_url + end + end + end + + def test_read_on_archived_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED + assert_failure "ls", svn_url + end + + def test_read_on_archived_private_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_ARCHIVED + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_failure "ls", svn_url + end + end + + def test_read_on_closed_projects_should_succeed + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + assert_success "ls", svn_url + end + + def test_read_on_closed_private_projects_should_succeed + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + Project.find(1).update_attribute :is_public, false + with_credentials "dlopper", "foo" do + assert_success "ls", svn_url + end + end + + def test_commit_on_closed_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + Role.find(2).add_permission! :commit_access + with_credentials "dlopper", "foo" do + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + def test_commit_on_closed_private_projects_should_fail + Project.find(1).update_attribute :status, Project::STATUS_CLOSED + Project.find(1).update_attribute :is_public, false + Role.find(2).add_permission! :commit_access + with_credentials "dlopper", "foo" do + assert_failure "mkdir --message Creating_a_directory", svn_url(random_filename) + end + end + + if ldap_configured? + def test_user_with_ldap_auth_source_should_authenticate_with_ldap_credentials + ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1) + ldap_user.login = 'example1' + ldap_user.save! + + with_settings :login_required => '1' do + with_credentials "example1", "123456" do + assert_success "ls", svn_url + end + end + + with_settings :login_required => '1' do + with_credentials "example1", "wrong" do + assert_failure "ls", svn_url + end + end + end + end + + def test_checkout + Dir.mktmpdir do |dir| + assert_success "checkout", svn_url, dir + end + end + + def test_read_commands + assert_success "info", svn_url + assert_success "ls", svn_url + assert_success "log", svn_url + end + + def test_write_commands + Role.find(2).add_permission! :commit_access + filename = random_filename + + Dir.mktmpdir do |dir| + assert_success "checkout", svn_url, dir + Dir.chdir(dir) do + # creates a file in the working copy + f = File.new(File.join(dir, filename), "w") + f.write "test file content" + f.close + + assert_success "add", filename + with_credentials "dlopper", "foo" do + assert_success "commit --message Committing_a_file" + assert_success "copy --message Copying_a_file", svn_url(filename), svn_url("#{filename}_copy") + assert_success "delete --message Deleting_a_file", svn_url(filename) + assert_success "mkdir --message Creating_a_directory", svn_url("#{filename}_dir") + end + assert_success "update" + + # checks that the working copy was updated + assert File.exists?(File.join(dir, "#{filename}_copy")) + assert File.directory?(File.join(dir, "#{filename}_dir")) + end + end + end + + def test_read_invalid_repo_should_fail + assert_failure "ls", svn_url("invalid") + end + + protected + + def execute(*args) + a = [SVN_BIN, "--no-auth-cache --non-interactive"] + a << "--username #{username}" if username + a << "--password #{password}" if password + + super a, *args + end + + def svn_url(path=nil) + host = ENV['REDMINE_TEST_DAV_SERVER'] || '127.0.0.1' + url = "http://#{host}/svn/ecookbook" + url << "/#{path}" if path + url + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0b/0b8467ea6271bd6bbf31748ec5f34d49b8671c8b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b8467ea6271bd6bbf31748ec5f34d49b8671c8b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0b/0b8a142f46ed608799d7faf6016772f4b1f09f42.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b8a142f46ed608799d7faf6016772f4b1f09f42.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAdminTest < ActionController::IntegrationTest + def test_administration_panel + assert_routing( + { :method => 'get', :path => "/admin" }, + { :controller => 'admin', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/admin/projects" }, + { :controller => 'admin', :action => 'projects' } + ) + assert_routing( + { :method => 'get', :path => "/admin/plugins" }, + { :controller => 'admin', :action => 'plugins' } + ) + assert_routing( + { :method => 'get', :path => "/admin/info" }, + { :controller => 'admin', :action => 'info' } + ) + assert_routing( + { :method => 'get', :path => "/admin/test_email" }, + { :controller => 'admin', :action => 'test_email' } + ) + assert_routing( + { :method => 'post', :path => "/admin/default_configuration" }, + { :controller => 'admin', :action => 'default_configuration' } + ) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0b/0bba8bba58df7fc70a5a93a0d2973b34736d639d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0bba8bba58df7fc70a5a93a0d2973b34736d639d.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,448 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'cgi' +require 'redmine/scm/adapters' + +if RUBY_VERSION < '1.9' + require 'iconv' +end + +module Redmine + module Scm + module Adapters + class AbstractAdapter #:nodoc: + + # raised if scm command exited with error, e.g. unknown revision. + class ScmCommandAborted < CommandFailed; end + + class << self + def client_command + "" + end + + def shell_quote_command + if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java' + client_command + else + shell_quote(client_command) + end + end + + # Returns the version of the scm client + # Eg: [1, 5, 0] or [] if unknown + def client_version + [] + end + + # Returns the version string of the scm client + # Eg: '1.5.0' or 'Unknown version' if unknown + def client_version_string + v = client_version || 'Unknown version' + v.is_a?(Array) ? v.join('.') : v.to_s + end + + # Returns true if the current client version is above + # or equals the given one + # If option is :unknown is set to true, it will return + # true if the client version is unknown + def client_version_above?(v, options={}) + ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown]) + end + + def client_available + true + end + + def shell_quote(str) + if Redmine::Platform.mswin? + '"' + str.gsub(/"/, '\\"') + '"' + else + "'" + str.gsub(/'/, "'\"'\"'") + "'" + end + end + end + + def initialize(url, root_url=nil, login=nil, password=nil, + path_encoding=nil) + @url = url + @login = login if login && !login.empty? + @password = (password || "") if @login + @root_url = root_url.blank? ? retrieve_root_url : root_url + end + + def adapter_name + 'Abstract' + end + + def supports_cat? + true + end + + def supports_annotate? + respond_to?('annotate') + end + + def root_url + @root_url + end + + def url + @url + end + + def path_encoding + nil + end + + # get info about the svn repository + def info + return nil + end + + # Returns the entry identified by path and revision identifier + # or nil if entry doesn't exist in the repository + 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) + es ? es.detect {|e| e.name == search_name} : nil + end + end + + # Returns an Entries collection + # or nil if the given path doesn't exist in the repository + def entries(path=nil, identifier=nil, options={}) + return nil + end + + def branches + return nil + end + + def tags + return nil + end + + def default_branch + return nil + end + + def properties(path, identifier=nil) + return nil + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + return nil + end + + def diff(path, identifier_from, identifier_to=nil) + return nil + end + + def cat(path, identifier=nil) + return nil + end + + def with_leading_slash(path) + path ||= '' + (path[0,1]!="/") ? "/#{path}" : path + end + + def with_trailling_slash(path) + path ||= '' + (path[-1,1] == "/") ? path : "#{path}/" + end + + def without_leading_slash(path) + path ||= '' + path.gsub(%r{^/+}, '') + end + + def without_trailling_slash(path) + path ||= '' + (path[-1,1] == "/") ? path[0..-2] : path + end + + def shell_quote(str) + self.class.shell_quote(str) + end + + private + def retrieve_root_url + info = self.info + info ? info.root_url : nil + end + + def target(path, sq=true) + path ||= '' + base = path.match(/^\//) ? root_url : url + str = "#{base}/#{path}".gsub(/[?<>\*]/, '') + if sq + str = shell_quote(str) + end + str + end + + def logger + self.class.logger + end + + def shellout(cmd, options = {}, &block) + self.class.shellout(cmd, options, &block) + end + + def self.logger + Rails.logger + end + + # Path to the file where scm stderr output is logged + # Returns nil if the log file is not writable + def self.stderr_log_file + if @stderr_log_file.nil? + writable = false + path = Redmine::Configuration['scm_stderr_log_file'].presence + path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s + if File.exists?(path) + if File.file?(path) && File.writable?(path) + writable = true + else + logger.warn("SCM log file (#{path}) is not writable") + end + else + begin + File.open(path, "w") {} + writable = true + rescue => e + logger.warn("SCM log file (#{path}) cannot be created: #{e.message}") + end + end + @stderr_log_file = writable ? path : false + end + @stderr_log_file || nil + end + + def self.shellout(cmd, options = {}, &block) + if logger && logger.debug? + logger.debug "Shelling out: #{strip_credential(cmd)}" + # Capture stderr in a log file + if stderr_log_file + cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}" + end + end + begin + mode = "r+" + IO.popen(cmd, mode) do |io| + io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding) + io.close_write unless options[:write_stdin] + block.call(io) if block_given? + end + ## If scm command does not exist, + ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException + ## in production environment. + # rescue Errno::ENOENT => e + rescue Exception => e + msg = strip_credential(e.message) + # The command failed, log it and re-raise + logmsg = "SCM command failed, " + logmsg += "make sure that your SCM command (e.g. svn) is " + logmsg += "in PATH (#{ENV['PATH']})\n" + logmsg += "You can configure your scm commands in config/configuration.yml.\n" + logmsg += "#{strip_credential(cmd)}\n" + logmsg += "with: #{msg}" + logger.error(logmsg) + raise CommandFailed.new(msg) + end + end + + # Hides username/password in a given command + def self.strip_credential(cmd) + q = (Redmine::Platform.mswin? ? '"' : "'") + cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx') + end + + def strip_credential(cmd) + self.class.strip_credential(cmd) + end + + def scm_iconv(to, from, str) + return nil if str.nil? + return str if to == from + if str.respond_to?(:force_encoding) + str.force_encoding(from) + begin + str.encode(to) + rescue Exception => err + logger.error("failed to convert from #{from} to #{to}. #{err}") + nil + end + else + begin + Iconv.conv(to, from, str) + rescue Iconv::Failure => err + logger.error("failed to convert from #{from} to #{to}. #{err}") + nil + end + end + end + + def parse_xml(xml) + if RUBY_PLATFORM == 'java' + xml = xml.sub(%r{<\?xml[^>]*\?>}, '') + end + ActiveSupport::XmlMini.parse(xml) + end + end + + class Entries < Array + def sort_by_name + dup.sort! {|x,y| + if x.kind == y.kind + x.name.to_s <=> y.name.to_s + else + x.kind <=> y.kind + end + } + end + + def revisions + revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact) + end + end + + class Info + attr_accessor :root_url, :lastrev + def initialize(attributes={}) + self.root_url = attributes[:root_url] if attributes[:root_url] + self.lastrev = attributes[:lastrev] + end + end + + class Entry + attr_accessor :name, :path, :kind, :size, :lastrev, :changeset + + def initialize(attributes={}) + self.name = attributes[:name] if attributes[:name] + self.path = attributes[:path] if attributes[:path] + self.kind = attributes[:kind] if attributes[:kind] + self.size = attributes[:size].to_i if attributes[:size] + self.lastrev = attributes[:lastrev] + end + + def is_file? + 'file' == self.kind + end + + def is_dir? + 'dir' == self.kind + end + + def is_text? + Redmine::MimeType.is_type?('text', name) + end + + def author + if changeset + changeset.author.to_s + elsif lastrev + Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first) + end + end + end + + class Revisions < Array + def latest + sort {|x,y| + unless x.time.nil? or y.time.nil? + x.time <=> y.time + else + 0 + end + }.last + end + end + + class Revision + attr_accessor :scmid, :name, :author, :time, :message, + :paths, :revision, :branch, :identifier, + :parents + + def initialize(attributes={}) + self.identifier = attributes[:identifier] + self.scmid = attributes[:scmid] + self.name = attributes[:name] || self.identifier + self.author = attributes[:author] + self.time = attributes[:time] + self.message = attributes[:message] || "" + self.paths = attributes[:paths] + self.revision = attributes[:revision] + self.branch = attributes[:branch] + self.parents = attributes[:parents] + end + + # Returns the readable identifier. + def format_identifier + self.identifier.to_s + end + + def ==(other) + if other.nil? + false + elsif scmid.present? + scmid == other.scmid + elsif identifier.present? + identifier == other.identifier + elsif revision.present? + revision == other.revision + end + end + end + + class Annotate + attr_reader :lines, :revisions + + def initialize + @lines = [] + @revisions = [] + end + + def add_line(line, revision) + @lines << line + @revisions << revision + end + + def content + content = lines.join("\n") + end + + def empty? + lines.empty? + end + end + + class Branch < String + attr_accessor :revision, :scmid + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0b/0bc8b19a332c3f8ffc74fbaa182a97b748ff7feb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0bc8b19a332c3f8ffc74fbaa182a97b748ff7feb.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class WatcherTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules, + :issues, :issue_statuses, :enumerations, :trackers, :projects_trackers, + :boards, :messages, + :wikis, :wiki_pages, + :watchers + + def setup + @user = User.find(1) + @issue = Issue.find(1) + end + + def test_validate + user = User.find(5) + assert !user.active? + watcher = Watcher.new(:user_id => user.id) + assert !watcher.save + end + + def test_watch + assert @issue.add_watcher(@user) + @issue.reload + assert @issue.watchers.detect { |w| w.user == @user } + end + + def test_cant_watch_twice + assert @issue.add_watcher(@user) + assert !@issue.add_watcher(@user) + end + + def test_watched_by + assert @issue.add_watcher(@user) + @issue.reload + assert @issue.watched_by?(@user) + assert Issue.watched_by(@user).include?(@issue) + end + + def test_watcher_users + watcher_users = Issue.find(2).watcher_users + assert_kind_of Array, watcher_users + assert_kind_of User, watcher_users.first + end + + def test_watcher_users_should_not_validate_user + User.update_all("firstname = ''", "id=1") + @user.reload + assert !@user.valid? + + issue = Issue.new(:project => Project.find(1), :tracker_id => 1, :subject => "test", :author => User.find(2)) + issue.watcher_users << @user + issue.save! + assert issue.watched_by?(@user) + end + + def test_watcher_user_ids + assert_equal [1, 3], Issue.find(2).watcher_user_ids.sort + end + + def test_watcher_user_ids= + issue = Issue.new + issue.watcher_user_ids = ['1', '3'] + assert issue.watched_by?(User.find(1)) + end + + def test_watcher_user_ids_should_make_ids_uniq + issue = Issue.new(:project => Project.find(1), :tracker_id => 1, :subject => "test", :author => User.find(2)) + issue.watcher_user_ids = ['1', '3', '1'] + issue.save! + assert_equal 2, issue.watchers.count + end + + def test_addable_watcher_users + addable_watcher_users = @issue.addable_watcher_users + assert_kind_of Array, addable_watcher_users + assert_kind_of User, addable_watcher_users.first + end + + def test_addable_watcher_users_should_not_include_user_that_cannot_view_the_object + issue = Issue.new(:project => Project.find(1), :is_private => true) + assert_nil issue.addable_watcher_users.detect {|user| !issue.visible?(user)} + end + + def test_recipients + @issue.watchers.delete_all + @issue.reload + + assert @issue.watcher_recipients.empty? + assert @issue.add_watcher(@user) + + @user.mail_notification = 'all' + @user.save! + @issue.reload + assert @issue.watcher_recipients.include?(@user.mail) + + @user.mail_notification = 'none' + @user.save! + @issue.reload + assert !@issue.watcher_recipients.include?(@user.mail) + end + + def test_unwatch + assert @issue.add_watcher(@user) + @issue.reload + assert_equal 1, @issue.remove_watcher(@user) + end + + def test_prune + Watcher.delete_all("user_id = 9") + user = User.find(9) + + # public + Watcher.create!(:watchable => Issue.find(1), :user => user) + Watcher.create!(:watchable => Issue.find(2), :user => user) + Watcher.create!(:watchable => Message.find(1), :user => user) + Watcher.create!(:watchable => Wiki.find(1), :user => user) + Watcher.create!(:watchable => WikiPage.find(2), :user => user) + + # private project (id: 2) + Member.create!(:project => Project.find(2), :principal => user, :role_ids => [1]) + 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) + + assert_no_difference 'Watcher.count' do + Watcher.prune(:user => User.find(9)) + end + + Member.delete_all + + assert_difference 'Watcher.count', -4 do + Watcher.prune(:user => User.find(9)) + end + + assert Issue.find(1).watched_by?(user) + assert !Issue.find(4).watched_by?(user) + end + + def test_prune_all + user = User.find(9) + Watcher.new(:watchable => Issue.find(4), :user => User.find(9)).save(:validate => false) + + assert Watcher.prune > 0 + assert !Issue.find(4).watched_by?(user) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0b/0bd8b54775e82ba1bfa004993bdb8bf956e85c99.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0bd8b54775e82ba1bfa004993bdb8bf956e85c99.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,137 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimeEntryQuery < Query + + self.queried_class = TimeEntry + + self.available_columns = [ + QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), + QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true), + QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), + QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true), + QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"), + QueryColumn.new(:comments), + QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours"), + ] + + def initialize(attributes=nil, *args) + super attributes + self.filters ||= {} + add_filter('spent_on', '*') unless filters.present? + end + + def initialize_available_filters + add_available_filter "spent_on", :type => :date_past + + principals = [] + if project + principals += project.principals.sort + unless project.leaf? + subprojects = project.descendants.visible.all + if subprojects.any? + add_available_filter "subproject_id", + :type => :list_subprojects, + :values => subprojects.collect{|s| [s.name, s.id.to_s] } + principals += Principal.member_of(subprojects) + end + end + else + if all_projects.any? + # members of visible projects + principals += Principal.member_of(all_projects) + # project filter + 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 + end + principals.uniq! + principals.sort! + users = principals.select {|p| p.is_a?(User)} + + users_values = [] + users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + users_values += users.collect{|s| [s.name, s.id.to_s] } + add_available_filter("user_id", + :type => :list_optional, :values => users_values + ) unless users_values.empty? + + activities = (project ? project.activities : TimeEntryActivity.shared.active) + add_available_filter("activity_id", + :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]} + ) unless activities.empty? + + add_available_filter "comments", :type => :text + add_available_filter "hours", :type => :float + + add_custom_fields_filters(TimeEntryCustomField) + add_associations_custom_fields_filters :project, :issue, :user + end + + def available_columns + return @available_columns if @available_columns + @available_columns = self.class.available_columns.dup + @available_columns += TimeEntryCustomField.visible.all.map {|cf| QueryCustomFieldColumn.new(cf) } + @available_columns += IssueCustomField.visible.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) } + @available_columns + end + + def default_columns_names + @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours] + end + + def results_scope(options={}) + order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) + + TimeEntry.visible. + where(statement). + order(order_option). + joins(joins_for_order_statement(order_option.join(','))). + includes(:activity) + end + + def sql_for_activity_id_field(field, operator, value) + condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id') + condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id') + ids = value.map(&:to_i).join(',') + table_name = Enumeration.table_name + if operator == '=' + "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))" + else + "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))" + end + end + + # Accepts :from/:to params as shortcut filters + def build_from_params(params) + super + if params[:from].present? && params[:to].present? + add_filter('spent_on', '><', [params[:from], params[:to]]) + elsif params[:from].present? + add_filter('spent_on', '>=', [params[:from]]) + elsif params[:to].present? + add_filter('spent_on', '<=', [params[:to]]) + end + self + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0c1341a53592418e4a2dd8783234ed2bf47a5fde.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c1341a53592418e4a2dd8783234ed2bf47a5fde.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1 @@ +$('#principals_for_new_member').html('<%= escape_javascript(render_principals_for_new_members(@project)) %>'); diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0c683f18d0752b5b1c900f3d3bf34e0652a0c13f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c683f18d0752b5b1c900f3d3bf34e0652a0c13f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,290 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class 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, 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.count(:all, :group => :status).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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0ca4bc46a7e22e354ba846105adab84836d7c7d6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0ca4bc46a7e22e354ba846105adab84836d7c7d6.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,780 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'active_record' +require 'iconv' if RUBY_VERSION < '1.9' +require 'pp' + +namespace :redmine do + desc 'Trac migration script' + task :migrate_from_trac => :environment do + + module TracMigrate + TICKET_MAP = [] + + DEFAULT_STATUS = IssueStatus.default + assigned_status = IssueStatus.find_by_position(2) + resolved_status = IssueStatus.find_by_position(3) + feedback_status = IssueStatus.find_by_position(4) + closed_status = IssueStatus.where(:is_closed => true).first + STATUS_MAPPING = {'new' => DEFAULT_STATUS, + 'reopened' => feedback_status, + 'assigned' => assigned_status, + 'closed' => closed_status + } + + priorities = IssuePriority.all + DEFAULT_PRIORITY = priorities[0] + PRIORITY_MAPPING = {'lowest' => priorities[0], + 'low' => priorities[0], + 'normal' => priorities[1], + 'high' => priorities[2], + 'highest' => priorities[3], + # --- + 'trivial' => priorities[0], + 'minor' => priorities[1], + 'major' => priorities[2], + 'critical' => priorities[3], + 'blocker' => priorities[4] + } + + TRACKER_BUG = Tracker.find_by_position(1) + TRACKER_FEATURE = Tracker.find_by_position(2) + DEFAULT_TRACKER = TRACKER_BUG + TRACKER_MAPPING = {'defect' => TRACKER_BUG, + 'enhancement' => TRACKER_FEATURE, + 'task' => TRACKER_FEATURE, + 'patch' =>TRACKER_FEATURE + } + + roles = Role.where(:builtin => 0).order('position ASC').all + manager_role = roles[0] + developer_role = roles[1] + DEFAULT_ROLE = roles.last + ROLE_MAPPING = {'admin' => manager_role, + 'developer' => developer_role + } + + class ::Time + class << self + alias :real_now :now + def now + real_now - @fake_diff.to_i + end + def fake(time) + @fake_diff = real_now - time + res = yield + @fake_diff = 0 + res + end + end + end + + class TracComponent < ActiveRecord::Base + self.table_name = :component + end + + class TracMilestone < ActiveRecord::Base + self.table_name = :milestone + # If this attribute is set a milestone has a defined target timepoint + def due + if read_attribute(:due) && read_attribute(:due) > 0 + Time.at(read_attribute(:due)).to_date + else + nil + end + end + # This is the real timepoint at which the milestone has finished. + def completed + if read_attribute(:completed) && read_attribute(:completed) > 0 + Time.at(read_attribute(:completed)).to_date + else + nil + end + end + + def description + # Attribute is named descr in Trac v0.8.x + has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description) + end + end + + class TracTicketCustom < ActiveRecord::Base + self.table_name = :ticket_custom + end + + class TracAttachment < ActiveRecord::Base + self.table_name = :attachment + set_inheritance_column :none + + def time; Time.at(read_attribute(:time)) end + + def original_filename + filename + end + + def content_type + '' + end + + def exist? + File.file? trac_fullpath + end + + def open + File.open("#{trac_fullpath}", 'rb') {|f| + @file = f + yield self + } + end + + def read(*args) + @file.read(*args) + end + + def description + read_attribute(:description).to_s.slice(0,255) + end + + private + def trac_fullpath + attachment_type = read_attribute(:type) + trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) } + "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}" + end + end + + class TracTicket < ActiveRecord::Base + self.table_name = :ticket + set_inheritance_column :none + + # ticket changes: only migrate status changes and comments + has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket + has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket + + def attachments + TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s]) + end + + def ticket_type + read_attribute(:type) + end + + def summary + read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary) + end + + def description + read_attribute(:description).blank? ? summary : read_attribute(:description) + end + + def time; Time.at(read_attribute(:time)) end + def changetime; Time.at(read_attribute(:changetime)) end + end + + class TracTicketChange < ActiveRecord::Base + self.table_name = :ticket_change + + def self.columns + # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0) + super.select {|column| column.name.to_s != 'field'} + end + + def time; Time.at(read_attribute(:time)) end + end + + TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \ + TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \ + TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \ + TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \ + TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \ + WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \ + CamelCase TitleIndex) + + class TracWikiPage < ActiveRecord::Base + self.table_name = :wiki + set_primary_key :name + + def self.columns + # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0) + super.select {|column| column.name.to_s != 'readonly'} + end + + def attachments + TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s]) + end + + def time; Time.at(read_attribute(:time)) end + end + + class TracPermission < ActiveRecord::Base + self.table_name = :permission + end + + class TracSessionAttribute < ActiveRecord::Base + self.table_name = :session_attribute + end + + def self.find_or_create_user(username, project_member = false) + return User.anonymous if username.blank? + + u = User.find_by_login(username) + if !u + # Create a new user if not found + mail = username[0, User::MAIL_LENGTH_LIMIT] + if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email') + mail = mail_attr.value + end + mail = "#{mail}@foo.bar" unless mail.include?("@") + + name = username + if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name') + name = name_attr.value + end + name =~ (/(\w+)(\s+\w+)?/) + fn = ($1 || "-").strip + ln = ($2 || '-').strip + + u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'), + :firstname => fn[0, limit_for(User, 'firstname')], + :lastname => ln[0, limit_for(User, 'lastname')] + + u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-') + u.password = 'trac' + u.admin = true if TracPermission.find_by_username_and_action(username, 'admin') + # finally, a default user is used if the new user is not valid + u = User.first unless u.save + end + # Make sure he is a member of the project + if project_member && !u.member_of?(@target_project) + role = DEFAULT_ROLE + if u.admin + role = ROLE_MAPPING['admin'] + elsif TracPermission.find_by_username_and_action(username, 'developer') + role = ROLE_MAPPING['developer'] + end + Member.create(:user => u, :project => @target_project, :roles => [role]) + u.reload + end + u + end + + # Basic wiki syntax conversion + def self.convert_wiki_text(text) + # Titles + text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"} + # External Links + text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"} + # Ticket links: + # [ticket:234 Text],[ticket:234 This is a test] + text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1') + # ticket:1234 + # #1 is working cause Redmine uses the same syntax. + text = text.gsub(/ticket\:([^\ ]+)/, '#\1') + # Milestone links: + # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)] + # The text "Milestone 0.1.0 (Mercury)" is not converted, + # cause Redmine's wiki does not support this. + text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"') + # [milestone:"0.1.0 Mercury"] + text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"') + text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"') + # milestone:0.1.0 + text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1') + text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1') + # Internal Links + text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below + text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"} + + # Links to pages UsingJustWikiCaps + text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]') + # Normalize things that were supposed to not be links + # like !NotALink + text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2') + # Revisions links + text = text.gsub(/\[(\d+)\]/, 'r\1') + # Ticket number re-writing + text = text.gsub(/#(\d+)/) do |s| + if $1.length < 10 +# TICKET_MAP[$1.to_i] ||= $1 + "\##{TICKET_MAP[$1.to_i] || $1}" + else + s + end + end + # We would like to convert the Code highlighting too + # This will go into the next line. + shebang_line = false + # Reguar expression for start of code + pre_re = /\{\{\{/ + # Code hightlighing... + shebang_re = /^\#\!([a-z]+)/ + # Regular expression for end of code + pre_end_re = /\}\}\}/ + + # Go through the whole text..extract it line by line + text = text.gsub(/^(.*)$/) do |line| + m_pre = pre_re.match(line) + if m_pre + line = '
'
+          else
+            m_sl = shebang_re.match(line)
+            if m_sl
+              shebang_line = true
+              line = ''
+            end
+            m_pre_end = pre_end_re.match(line)
+            if m_pre_end
+              line = '
' + if shebang_line + line = '' + line + end + end + end + line + end + + # Highlighting + text = text.gsub(/'''''([^\s])/, '_*\1') + text = text.gsub(/([^\s])'''''/, '\1*_') + text = text.gsub(/'''/, '*') + text = text.gsub(/''/, '_') + text = text.gsub(/__/, '+') + text = text.gsub(/~~/, '-') + text = text.gsub(/`/, '@') + text = text.gsub(/,,/, '~') + # Lists + text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "} + + text + end + + def self.migrate + establish_connection + + # Quick database test + TracComponent.count + + migrated_components = 0 + migrated_milestones = 0 + migrated_tickets = 0 + migrated_custom_values = 0 + migrated_ticket_attachments = 0 + migrated_wiki_edits = 0 + migrated_wiki_attachments = 0 + + #Wiki system initializing... + @target_project.wiki.destroy if @target_project.wiki + @target_project.reload + wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart') + wiki_edit_count = 0 + + # Components + print "Migrating components" + issues_category_map = {} + TracComponent.all.each do |component| + print '.' + STDOUT.flush + c = IssueCategory.new :project => @target_project, + :name => encode(component.name[0, limit_for(IssueCategory, 'name')]) + next unless c.save + issues_category_map[component.name] = c + migrated_components += 1 + end + puts + + # Milestones + print "Migrating milestones" + version_map = {} + TracMilestone.all.each do |milestone| + print '.' + STDOUT.flush + # First we try to find the wiki page... + p = wiki.find_or_new_page(milestone.name.to_s) + p.content = WikiContent.new(:page => p) if p.new_record? + p.content.text = milestone.description.to_s + p.content.author = find_or_create_user('trac') + p.content.comments = 'Milestone' + p.save + + v = Version.new :project => @target_project, + :name => encode(milestone.name[0, limit_for(Version, 'name')]), + :description => nil, + :wiki_page_title => milestone.name.to_s, + :effective_date => milestone.completed + + next unless v.save + version_map[milestone.name] = v + migrated_milestones += 1 + end + puts + + # Custom fields + # TODO: read trac.ini instead + print "Migrating custom fields" + custom_field_map = {} + TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field| + print '.' + STDOUT.flush + # Redmine custom field name + field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize + # Find if the custom already exists in Redmine + f = IssueCustomField.find_by_name(field_name) + # Or create a new one + f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, + :field_format => 'string') + + next if f.new_record? + f.trackers = Tracker.all + f.projects << @target_project + custom_field_map[field.name] = f + end + puts + + # Trac 'resolution' field as a Redmine custom field + r = IssueCustomField.where(:name => "Resolution").first + r = IssueCustomField.new(:name => 'Resolution', + :field_format => 'list', + :is_filter => true) if r.nil? + r.trackers = Tracker.all + r.projects << @target_project + r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq + r.save! + custom_field_map['resolution'] = r + + # Tickets + print "Migrating tickets" + TracTicket.find_each(:batch_size => 200) do |ticket| + print '.' + STDOUT.flush + i = Issue.new :project => @target_project, + :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]), + :description => convert_wiki_text(encode(ticket.description)), + :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY, + :created_on => ticket.time + i.author = find_or_create_user(ticket.reporter) + i.category = issues_category_map[ticket.component] unless ticket.component.blank? + i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank? + i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS + i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER + i.id = ticket.id unless Issue.exists?(ticket.id) + next unless Time.fake(ticket.changetime) { i.save } + TICKET_MAP[ticket.id] = i.id + migrated_tickets += 1 + + # Owner + unless ticket.owner.blank? + i.assigned_to = find_or_create_user(ticket.owner, true) + Time.fake(ticket.changetime) { i.save } + end + + # Comments and status/resolution changes + ticket.ticket_changes.group_by(&:time).each do |time, changeset| + status_change = changeset.select {|change| change.field == 'status'}.first + resolution_change = changeset.select {|change| change.field == 'resolution'}.first + comment_change = changeset.select {|change| change.field == 'comment'}.first + + n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''), + :created_on => time + n.user = find_or_create_user(changeset.first.author) + n.journalized = i + if status_change && + STATUS_MAPPING[status_change.oldvalue] && + STATUS_MAPPING[status_change.newvalue] && + (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue]) + n.details << JournalDetail.new(:property => 'attr', + :prop_key => 'status_id', + :old_value => STATUS_MAPPING[status_change.oldvalue].id, + :value => STATUS_MAPPING[status_change.newvalue].id) + end + if resolution_change + n.details << JournalDetail.new(:property => 'cf', + :prop_key => custom_field_map['resolution'].id, + :old_value => resolution_change.oldvalue, + :value => resolution_change.newvalue) + end + n.save unless n.details.empty? && n.notes.blank? + end + + # Attachments + ticket.attachments.each do |attachment| + next unless attachment.exist? + attachment.open { + a = Attachment.new :created_on => attachment.time + a.file = attachment + a.author = find_or_create_user(attachment.author) + a.container = i + a.description = attachment.description + migrated_ticket_attachments += 1 if a.save + } + end + + # Custom fields + custom_values = ticket.customs.inject({}) do |h, custom| + if custom_field = custom_field_map[custom.name] + h[custom_field.id] = custom.value + migrated_custom_values += 1 + end + h + end + if custom_field_map['resolution'] && !ticket.resolution.blank? + custom_values[custom_field_map['resolution'].id] = ticket.resolution + end + i.custom_field_values = custom_values + i.save_custom_field_values + end + + # update issue id sequence if needed (postgresql) + Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!') + puts + + # Wiki + print "Migrating wiki" + if wiki.save + TracWikiPage.order('name, version').all.each do |page| + # Do not migrate Trac manual wiki pages + next if TRAC_WIKI_PAGES.include?(page.name) + wiki_edit_count += 1 + print '.' + STDOUT.flush + p = wiki.find_or_new_page(page.name) + p.content = WikiContent.new(:page => p) if p.new_record? + p.content.text = page.text + p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac' + p.content.comments = page.comment + Time.fake(page.time) { p.new_record? ? p.save : p.content.save } + + next if p.content.new_record? + migrated_wiki_edits += 1 + + # Attachments + page.attachments.each do |attachment| + next unless attachment.exist? + next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page + attachment.open { + a = Attachment.new :created_on => attachment.time + a.file = attachment + a.author = find_or_create_user(attachment.author) + a.description = attachment.description + a.container = p + migrated_wiki_attachments += 1 if a.save + } + end + end + + wiki.reload + wiki.pages.each do |page| + page.content.text = convert_wiki_text(page.content.text) + Time.fake(page.content.updated_on) { page.content.save } + end + end + puts + + puts + puts "Components: #{migrated_components}/#{TracComponent.count}" + puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}" + puts "Tickets: #{migrated_tickets}/#{TracTicket.count}" + puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s + puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}" + puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}" + puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s + end + + def self.limit_for(klass, attribute) + klass.columns_hash[attribute.to_s].limit + end + + def self.encoding(charset) + @charset = charset + end + + def self.set_trac_directory(path) + @@trac_directory = path + raise "This directory doesn't exist!" unless File.directory?(path) + raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory) + @@trac_directory + rescue Exception => e + puts e + return false + end + + def self.trac_directory + @@trac_directory + end + + def self.set_trac_adapter(adapter) + return false if adapter.blank? + raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter) + # If adapter is sqlite or sqlite3, make sure that trac.db exists + raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path) + @@trac_adapter = adapter + rescue Exception => e + puts e + return false + end + + def self.set_trac_db_host(host) + return nil if host.blank? + @@trac_db_host = host + end + + def self.set_trac_db_port(port) + return nil if port.to_i == 0 + @@trac_db_port = port.to_i + end + + def self.set_trac_db_name(name) + return nil if name.blank? + @@trac_db_name = name + end + + def self.set_trac_db_username(username) + @@trac_db_username = username + end + + def self.set_trac_db_password(password) + @@trac_db_password = password + end + + def self.set_trac_db_schema(schema) + @@trac_db_schema = schema + end + + mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password + + def self.trac_db_path; "#{trac_directory}/db/trac.db" end + def self.trac_attachments_directory; "#{trac_directory}/attachments" end + + def self.target_project_identifier(identifier) + project = Project.find_by_identifier(identifier) + if !project + # create the target project + project = Project.new :name => identifier.humanize, + :description => '' + project.identifier = identifier + puts "Unable to create a project with identifier '#{identifier}'!" unless project.save + # enable issues and wiki for the created project + project.enabled_module_names = ['issue_tracking', 'wiki'] + else + puts + puts "This project already exists in your Redmine database." + print "Are you sure you want to append data to this project ? [Y/n] " + STDOUT.flush + exit if STDIN.gets.match(/^n$/i) + end + project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG) + project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE) + @target_project = project.new_record? ? nil : project + @target_project.reload + end + + def self.connection_params + if trac_adapter == 'sqlite3' + {:adapter => 'sqlite3', + :database => trac_db_path} + else + {:adapter => trac_adapter, + :database => trac_db_name, + :host => trac_db_host, + :port => trac_db_port, + :username => trac_db_username, + :password => trac_db_password, + :schema_search_path => trac_db_schema + } + end + end + + def self.establish_connection + constants.each do |const| + klass = const_get(const) + next unless klass.respond_to? 'establish_connection' + klass.establish_connection connection_params + end + end + + def self.encode(text) + if RUBY_VERSION < '1.9' + @ic ||= Iconv.new('UTF-8', @charset) + @ic.iconv text + else + text.to_s.force_encoding(@charset).encode('UTF-8') + end + end + end + + puts + if Redmine::DefaultData::Loader.no_data? + puts "Redmine configuration need to be loaded before importing data." + puts "Please, run this first:" + puts + puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\"" + exit + end + + puts "WARNING: a new project will be added to Redmine during this process." + print "Are you sure you want to continue ? [y/N] " + STDOUT.flush + break unless STDIN.gets.match(/^y$/i) + puts + + def prompt(text, options = {}, &block) + default = options[:default] || '' + while true + print "#{text} [#{default}]: " + STDOUT.flush + value = STDIN.gets.chomp! + value = default if value.blank? + break if yield value + end + end + + DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432} + + prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip} + prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter} + unless %w(sqlite3).include?(TracMigrate.trac_adapter) + prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host} + prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port} + prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name} + prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema} + prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username} + prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password} + end + prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding} + prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier} + puts + + old_notified_events = Setting.notified_events + old_password_min_length = Setting.password_min_length + begin + # Turn off email notifications temporarily + Setting.notified_events = [] + Setting.password_min_length = 4 + # Run the migration + TracMigrate.migrate + ensure + # Restore previous settings + Setting.notified_events = old_notified_events + Setting.password_min_length = old_password_min_length + end + end +end + diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0ca7bda81025c5f7a9ebad14d8b0c08f3a0b8176.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0ca7bda81025c5f7a9ebad14d8b0c08f3a0b8176.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,35 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingSysTest < ActionController::IntegrationTest + def test_sys + assert_routing( + { :method => 'get', :path => "/sys/projects" }, + { :controller => 'sys', :action => 'projects' } + ) + assert_routing( + { :method => 'post', :path => "/sys/projects/testid/repository" }, + { :controller => 'sys', :action => 'create_project_repository', :id => 'testid' } + ) + assert_routing( + { :method => 'get', :path => "/sys/fetch_changesets" }, + { :controller => 'sys', :action => 'fetch_changesets' } + ) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0cb5ab49f6fd89443044a7c2f37992f0e2874866.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0cb5ab49f6fd89443044a7c2f37992f0e2874866.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,257 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/git_adapter' + +class Repository::Git < Repository + attr_protected :root_url + validates_presence_of :url + + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s + if attr_name == "url" + attr_name = "path_to_repository" + end + super(attr_name, *args) + end + + def self.scm_adapter_class + Redmine::Scm::Adapters::GitAdapter + end + + def self.scm_name + 'Git' + end + + def report_last_commit + extra_report_last_commit + end + + def extra_report_last_commit + return false if extra_info.nil? + v = extra_info["extra_report_last_commit"] + return false if v.nil? + v.to_s != '0' + end + + def supports_directory_revisions? + true + end + + def supports_revision_graph? + true + end + + def repo_log_encoding + 'UTF-8' + end + + # Returns the identifier for the given git changeset + def self.changeset_identifier(changeset) + changeset.scmid + end + + # Returns the readable identifier for the given git changeset + def self.format_changeset_identifier(changeset) + changeset.revision[0, 8] + end + + def branches + scm.branches + end + + def tags + scm.tags + end + + def default_branch + scm.default_branch + rescue Exception => e + logger.error "git: error during get default branch: #{e.message}" + nil + end + + def find_changeset_by_name(name) + if name.present? + changesets.where(:revision => name.to_s).first || + changesets.where('scmid LIKE ?', "#{name}%").first + end + end + + def entries(path=nil, identifier=nil) + entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit) + load_entries_changesets(entries) + entries + end + + # With SCMs that have a sequential commit numbering, + # such as Subversion and Mercurial, + # Redmine is able to be clever and only fetch changesets + # going forward from the most recent one it knows about. + # + # However, Git does not have a sequential commit numbering. + # + # In order to fetch only new adding revisions, + # Redmine needs to save "heads". + # + # In Git and Mercurial, revisions are not in date order. + # Redmine Mercurial fixed issues. + # * Redmine Takes Too Long On Large Mercurial Repository + # http://www.redmine.org/issues/3449 + # * Sorting for changesets might go wrong on Mercurial repos + # http://www.redmine.org/issues/3567 + # + # Database revision column is text, so Redmine can not sort by revision. + # Mercurial has revision number, and revision number guarantees revision order. + # Redmine Mercurial model stored revisions ordered by database id to database. + # So, Redmine Mercurial model can use correct ordering revisions. + # + # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10" + # to get limited revisions from old to new. + # But, Git 1.7.3.4 does not support --reverse with -n or --skip. + # + # The repository can still be fully reloaded by calling #clear_changesets + # before fetching changesets (eg. for offline resync) + def fetch_changesets + scm_brs = branches + return if scm_brs.nil? || scm_brs.empty? + + h1 = extra_info || {} + h = h1.dup + repo_heads = scm_brs.map{ |br| br.scmid } + h["heads"] ||= [] + prev_db_heads = h["heads"].dup + if prev_db_heads.empty? + prev_db_heads += heads_from_branches_hash + end + return if prev_db_heads.sort == repo_heads.sort + + h["db_consistent"] ||= {} + if changesets.count == 0 + h["db_consistent"]["ordering"] = 1 + merge_extra_info(h) + self.save + elsif ! h["db_consistent"].has_key?("ordering") + h["db_consistent"]["ordering"] = 0 + merge_extra_info(h) + self.save + end + save_revisions(prev_db_heads, repo_heads) + end + + def save_revisions(prev_db_heads, repo_heads) + h = {} + opts = {} + opts[:reverse] = true + opts[:excludes] = prev_db_heads + opts[:includes] = repo_heads + + revisions = scm.revisions('', nil, nil, opts) + return if revisions.blank? + + # Make the search for existing revisions in the database in a more sufficient manner + # + # Git branch is the reference to the specific revision. + # Git can *delete* remote branch and *re-push* branch. + # + # $ git push remote :branch + # $ git push remote branch + # + # After deleting branch, revisions remain in repository until "git gc". + # On git 1.7.2.3, default pruning date is 2 weeks. + # So, "git log --not deleted_branch_head_revision" return code is 0. + # + # After re-pushing branch, "git log" returns revisions which are saved in database. + # So, Redmine needs to scan revisions and database every time. + # + # This is replacing the one-after-one queries. + # Find all revisions, that are in the database, and then remove them from the revision array. + # Then later we won't need any conditions for db existence. + # Query for several revisions at once, and remove them from the revisions array, if they are there. + # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits). + # If there are no revisions (because the original code's algorithm filtered them), + # then this part will be stepped over. + # We make queries, just if there is any revision. + limit = 100 + offset = 0 + revisions_copy = revisions.clone # revisions will change + while offset < revisions_copy.size + scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid} + recent_changesets_slice = changesets.where(:scmid => scmids).all + # Subtract revisions that redmine already knows about + recent_revisions = recent_changesets_slice.map{|c| c.scmid} + revisions.reject!{|r| recent_revisions.include?(r.scmid)} + offset += limit + end + + revisions.each do |rev| + transaction do + # There is no search in the db for this revision, because above we ensured, + # that it's not in the db. + save_revision(rev) + end + end + h["heads"] = repo_heads.dup + merge_extra_info(h) + self.save + end + private :save_revisions + + def save_revision(rev) + parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact + changeset = Changeset.create( + :repository => self, + :revision => rev.identifier, + :scmid => rev.scmid, + :committer => rev.author, + :committed_on => rev.time, + :comments => rev.message, + :parents => parents + ) + unless changeset.new_record? + rev.paths.each { |change| changeset.create_change(change) } + end + changeset + end + private :save_revision + + def heads_from_branches_hash + h1 = extra_info || {} + h = h1.dup + h["branches"] ||= {} + h['branches'].map{|br, hs| hs['last_scmid']} + end + + def latest_changesets(path,rev,limit=10) + revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false) + return [] if revisions.nil? || revisions.empty? + + changesets.where(:scmid => revisions.map {|c| c.scmid}).all + end + + def clear_extra_info_of_changesets + return if extra_info.nil? + v = extra_info["extra_report_last_commit"] + write_attribute(:extra_info, nil) + h = {} + h["extra_report_last_commit"] = v + merge_extra_info(h) + self.save + end + private :clear_extra_info_of_changesets +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0c/0cc9601ec1a6e3a27ab4a946d394cf4ba82254b4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0cc9601ec1a6e3a27ab4a946d394cf4ba82254b4.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimeEntryActivityCustomField < CustomField + def type_name + :enumeration_activities + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d408463a01e894d3f86946e8b5d1925b0335095.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d408463a01e894d3f86946e8b5d1925b0335095.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,14 @@ +var fileSpan = $('#attachments_<%= j params[:attachment_id] %>'); +<% if @attachment.new_record? %> + fileSpan.hide(); + alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>"); +<% else %> +$('', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan); +fileSpan.find('a.remove-upload') + .attr({ + "data-remote": true, + "data-method": 'delete', + href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>' + }) + .off('click'); +<% end %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d422b89acdac2bb8c5929f11e22f0904e9f370b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d422b89acdac2bb8c5929f11e22f0904e9f370b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,74 @@ +api.issue do + api.id @issue.id + api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil? + api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil? + api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil? + api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil? + api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil? + api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil? + api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil? + api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil? + api.parent(:id => @issue.parent_id) unless @issue.parent.nil? + + api.subject @issue.subject + api.description @issue.description + api.start_date @issue.start_date + api.due_date @issue.due_date + api.done_ratio @issue.done_ratio + api.estimated_hours @issue.estimated_hours + api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project) + + render_api_custom_values @issue.visible_custom_field_values, api + + api.created_on @issue.created_on + api.updated_on @issue.updated_on + api.closed_on @issue.closed_on + + render_api_issue_children(@issue, api) if include_in_api_response?('children') + + api.array :attachments do + @issue.attachments.each do |attachment| + render_api_attachment(attachment, api) + end + end if include_in_api_response?('attachments') + + api.array :relations do + @relations.each do |relation| + api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay) + end + end if include_in_api_response?('relations') && @relations.present? + + api.array :changesets do + @issue.changesets.each do |changeset| + api.changeset :revision => changeset.revision do + api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil? + api.comments changeset.comments + api.committed_on changeset.committed_on + end + end + end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project) + + api.array :journals do + @journals.each do |journal| + api.journal :id => journal.id do + api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil? + api.notes journal.notes + api.created_on journal.created_on + api.array :details do + journal.visible_details.each do |detail| + api.detail :property => detail.property, :name => detail.prop_key do + api.old_value detail.old_value + api.new_value detail.value + end + end + end + end + end + end if include_in_api_response?('journals') + + api.array :watchers do + @issue.watcher_users.each do |user| + api.user :id => user.id, :name => user.name + end + end if include_in_api_response?('watchers') && User.current.allowed_to?(:view_issue_watchers, @issue.project) +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d50a39088b6bd107e54b315b0bf77a7ffa63596.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d50a39088b6bd107e54b315b0bf77a7ffa63596.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,110 @@ +### From http://svn.geekdaily.org/public/rails/plugins/generally_useful/tasks/coverage_via_rcov.rake + +namespace :test do + desc 'Measures test coverage' + task :coverage do + rm_f "coverage" + rm_f "coverage.data" + rcov = "rcov --rails --aggregate coverage.data --text-summary -Ilib --html --exclude gems/" + files = %w(unit functional integration).map {|dir| Dir.glob("test/#{dir}/**/*_test.rb")}.flatten.join(" ") + system("#{rcov} #{files}") + end + + desc 'Run unit and functional scm tests' + task :scm do + errors = %w(test:scm:units test:scm:functionals).collect do |task| + begin + Rake::Task[task].invoke + nil + rescue => e + task + end + end.compact + abort "Errors running #{errors.to_sentence(:locale => :en)}!" if errors.any? + end + + namespace :scm do + namespace :setup do + desc "Creates directory for test repositories" + task :create_dir do + FileUtils.mkdir_p Rails.root + '/tmp/test' + end + + supported_scms = [:subversion, :cvs, :bazaar, :mercurial, :git, :darcs, :filesystem] + + desc "Creates a test subversion repository" + task :subversion => :create_dir do + repo_path = "tmp/test/subversion_repository" + unless File.exists?(repo_path) + system "svnadmin create #{repo_path}" + system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}" + end + end + + desc "Creates a test mercurial repository" + task :mercurial => :create_dir do + repo_path = "tmp/test/mercurial_repository" + unless File.exists?(repo_path) + bundle_path = "test/fixtures/repositories/mercurial_repository.hg" + system "hg init #{repo_path}" + system "hg -R #{repo_path} pull #{bundle_path}" + end + end + + (supported_scms - [:subversion, :mercurial]).each do |scm| + desc "Creates a test #{scm} repository" + task scm => :create_dir do + unless File.exists?("tmp/test/#{scm}_repository") + # system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test" + system "tar -xvz -C tmp/test -f test/fixtures/repositories/#{scm}_repository.tar.gz" + end + end + end + + desc "Creates all test repositories" + task :all => supported_scms + end + + desc "Updates installed test repositories" + task :update do + require 'fileutils' + Dir.glob("tmp/test/*_repository").each do |dir| + next unless File.basename(dir) =~ %r{^(.+)_repository$} && File.directory?(dir) + scm = $1 + next unless fixture = Dir.glob("test/fixtures/repositories/#{scm}_repository.*").first + next if File.stat(dir).ctime > File.stat(fixture).mtime + + FileUtils.rm_rf dir + Rake::Task["test:scm:setup:#{scm}"].execute + end + end + + Rake::TestTask.new(:units => "db:test:prepare") do |t| + t.libs << "test" + t.verbose = true + t.test_files = FileList['test/unit/repository*_test.rb'] + FileList['test/unit/lib/redmine/scm/**/*_test.rb'] + end + Rake::Task['test:scm:units'].comment = "Run the scm unit tests" + + Rake::TestTask.new(:functionals => "db:test:prepare") do |t| + t.libs << "test" + t.verbose = true + t.test_files = FileList['test/functional/repositories*_test.rb'] + end + Rake::Task['test:scm:functionals'].comment = "Run the scm functional tests" + end + + Rake::TestTask.new(:rdm_routing) do |t| + t.libs << "test" + t.verbose = true + t.test_files = FileList['test/integration/routing/*_test.rb'] + end + Rake::Task['test:rdm_routing'].comment = "Run the routing tests" + + Rake::TestTask.new(:ui => "db:test:prepare") do |t| + t.libs << "test" + t.verbose = true + t.test_files = FileList['test/ui/**/*_test.rb'] + end + Rake::Task['test:ui'].comment = "Run the UI tests with Capybara (PhantomJS listening on port 4444 is required)" +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0d6f6e567915dec9c4fe1362362e16f727a01572.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0d6f6e567915dec9c4fe1362362e16f727a01572.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,38 @@ +

<%= l(:label_board_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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0d/0de0220c6abf526fd90b0775dd5b7d3c897e479b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0d/0de0220c6abf526fd90b0775dd5b7d3c897e479b.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0e/0e9c8f11b3db41702079f085c94dfa01133abcd9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0e9c8f11b3db41702079f085c94dfa01133abcd9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,128 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class PdfTest < ActiveSupport::TestCase + fixtures :users, :projects, :roles, :members, :member_roles, + :enabled_modules, :issues, :trackers, :attachments + + def test_fix_text_encoding_nil + assert_equal '', Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(nil, "UTF-8") + assert_equal '', Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(nil, "ISO-8859-1") + end + + def test_rdm_pdf_iconv_cannot_convert_ja_cp932 + encoding = ( RUBY_PLATFORM == 'java' ? "SJIS" : "CP932" ) + utf8_txt_1 = "\xe7\x8b\x80\xe6\x85\x8b" + utf8_txt_2 = "\xe7\x8b\x80\xe6\x85\x8b\xe7\x8b\x80" + utf8_txt_3 = "\xe7\x8b\x80\xe7\x8b\x80\xe6\x85\x8b\xe7\x8b\x80" + if utf8_txt_1.respond_to?(:force_encoding) + 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".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 + elsif RUBY_PLATFORM == 'java' + assert_equal "??", + Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_1, encoding) + assert_equal "???", + Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_2, encoding) + assert_equal "????", + Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_3, encoding) + else + assert_equal "???\x91\xd4", + Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_1, encoding) + assert_equal "???\x91\xd4???", + Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_2, encoding) + assert_equal "??????\x91\xd4???", + Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(utf8_txt_3, encoding) + end + end + + def test_rdm_pdf_iconv_invalid_utf8_should_be_replaced_en + 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) + txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str1, 'UTF-8') + txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str2, 'UTF-8') + if txt_1.respond_to?(:force_encoding) + assert_equal "ASCII-8BIT", txt_1.encoding.to_s + assert_equal "ASCII-8BIT", txt_2.encoding.to_s + end + assert_equal "Texte encod? en ISO-8859-1", txt_1 + assert_equal "?a?b?c?d?e test", txt_2 + end + + def test_rdm_pdf_iconv_invalid_utf8_should_be_replaced_ja + 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) + encoding = ( RUBY_PLATFORM == 'java' ? "SJIS" : "CP932" ) + txt_1 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str1, encoding) + txt_2 = Redmine::Export::PDF::RDMPdfEncoding::rdm_from_utf8(str2, encoding) + if txt_1.respond_to?(:force_encoding) + assert_equal "ASCII-8BIT", txt_1.encoding.to_s + assert_equal "ASCII-8BIT", txt_2.encoding.to_s + end + assert_equal "Texte encod? en ISO-8859-1", txt_1 + assert_equal "?a?b?c?d?e test", txt_2 + end + + def test_attach + set_fixtures_attachments_directory + + str2 = "\x83e\x83X\x83g" + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + encoding = ( RUBY_PLATFORM == 'java' ? "SJIS" : "CP932" ) + + a1 = Attachment.find(17) + a2 = Attachment.find(19) + + User.current = User.find(1) + assert a1.readable? + assert a1.visible? + assert a2.readable? + assert a2.visible? + + aa1 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "Testfile.PNG", "UTF-8") + assert_not_nil aa1 + assert_equal 17, aa1.id + aa2 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "test#{str2}.png", encoding) + assert_not_nil aa2 + assert_equal 19, aa2.id + + User.current = nil + assert a1.readable? + assert (! a1.visible?) + assert a2.readable? + assert (! a2.visible?) + + aa1 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "Testfile.PNG", "UTF-8") + assert_equal nil, aa1 + aa2 = Redmine::Export::PDF::RDMPdfEncoding::attach(Attachment.all, "test#{str2}.png", encoding) + assert_equal nil, aa2 + + set_tmp_attachments_directory + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0e/0eb716c2e3251d0a363271012989e30efeb34f17.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0eb716c2e3251d0a363271012989e30efeb34f17.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,3 @@ +<%= form_tag(signout_path) do %> +

<%= submit_tag l(:label_logout) %>

+<% end %> diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0e/0ed2a8155e8187c04d224caafbc3970f5a91d733.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0ed2a8155e8187c04d224caafbc3970f5a91d733.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,198 @@ +

<%= @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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0e/0efedaea2e714f5dbe3b6a1b0cd102c208875d70.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0efedaea2e714f5dbe3b6a1b0cd102c208875d70.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,1104 @@ +# Chinese (China) translations for Ruby on Rails +# by tsechingho (http://github.com/tsechingho) +zh: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + jquery: + locale: "zh-CN" + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b%dæ—¥" + long: "%Yå¹´%b%dæ—¥" + + day_names: [星期天, 星期一, 星期二, 星期三, 星期四, 星期五, 星期六] + abbr_day_names: [æ—¥, 一, 二, 三, å››, 五, å…­] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, 一月, 二月, 三月, 四月, 五月, 六月, 七月, 八月, 乿œˆ, åæœˆ, å一月, å二月] + abbr_month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Yå¹´%b%dæ—¥ %A %H:%M:%S" + time: "%H:%M" + short: "%b%dæ—¥ %H:%M" + long: "%Yå¹´%b%dæ—¥ %H:%M" + am: "上åˆ" + pm: "下åˆ" + + datetime: + distance_in_words: + half_a_minute: "åŠåˆ†é’Ÿ" + less_than_x_seconds: + one: "一秒内" + other: "少于 %{count} ç§’" + x_seconds: + one: "一秒" + other: "%{count} ç§’" + less_than_x_minutes: + one: "一分钟内" + other: "少于 %{count} 分钟" + x_minutes: + one: "一分钟" + other: "%{count} 分钟" + about_x_hours: + one: "å¤§çº¦ä¸€å°æ—¶" + other: "大约 %{count} å°æ—¶" + x_hours: + one: "1 å°æ—¶" + other: "%{count} å°æ—¶" + x_days: + one: "一天" + other: "%{count} 天" + about_x_months: + one: "大约一个月" + other: "大约 %{count} 个月" + x_months: + one: "一个月" + other: "%{count} 个月" + about_x_years: + one: "大约一年" + other: "大约 %{count} å¹´" + over_x_years: + one: "超过一年" + other: "超过 %{count} å¹´" + almost_x_years: + one: "将近 1 å¹´" + other: "将近 %{count} å¹´" + + number: + # Default format for numbers + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "å’Œ" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "由于å‘生了一个错误 %{model} 无法ä¿å­˜" + other: "%{count} 个错误使得 %{model} 无法ä¿å­˜" + messages: + inclusion: "ä¸åŒ…å«äºŽåˆ—表中" + exclusion: "是ä¿ç•™å…³é”®å­—" + invalid: "是无效的" + confirmation: "与确认值ä¸åŒ¹é…" + accepted: "必须是å¯è¢«æŽ¥å—çš„" + empty: "ä¸èƒ½ç•™ç©º" + blank: "ä¸èƒ½ä¸ºç©ºå­—符" + too_long: "过长(最长为 %{count} 个字符)" + too_short: "过短(最短为 %{count} 个字符)" + wrong_length: "é•¿åº¦éžæ³•(必须为 %{count} 个字符)" + taken: "å·²ç»è¢«ä½¿ç”¨" + not_a_number: "䏿˜¯æ•°å­—" + not_a_date: "䏿˜¯åˆæ³•日期" + greater_than: "必须大于 %{count}" + greater_than_or_equal_to: "必须大于或等于 %{count}" + equal_to: "必须等于 %{count}" + less_than: "å¿…é¡»å°äºŽ %{count}" + less_than_or_equal_to: "å¿…é¡»å°äºŽæˆ–等于 %{count}" + odd: "å¿…é¡»ä¸ºå•æ•°" + even: "å¿…é¡»ä¸ºåŒæ•°" + greater_than_start_date: "必须在起始日期之åŽ" + not_same_project: "ä¸å±žäºŽåŒä¸€ä¸ªé¡¹ç›®" + circular_dependency: "此关è”将导致循环ä¾èµ–" + cant_link_an_issue_with_a_descendant: "问题ä¸èƒ½å…³è”到它的å­ä»»åŠ¡" + earlier_than_minimum_start_date: "cannot be earlier than %{date} because of preceding issues" + + actionview_instancetag_blank_option: 请选择 + + general_text_No: 'å¦' + general_text_Yes: '是' + general_text_no: 'å¦' + general_text_yes: '是' + general_lang_name: 'Simplified Chinese (简体中文)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: gb18030 + general_pdf_encoding: gb18030 + general_first_day_of_week: '7' + + notice_account_updated: å¸å·æ›´æ–°æˆåŠŸ + notice_account_invalid_creditentials: æ— æ•ˆçš„ç”¨æˆ·åæˆ–å¯†ç  + notice_account_password_updated: å¯†ç æ›´æ–°æˆåŠŸ + notice_account_wrong_password: 密ç é”™è¯¯ + notice_account_register_done: å¸å·åˆ›å»ºæˆåŠŸï¼Œè¯·ä½¿ç”¨æ³¨å†Œç¡®è®¤é‚®ä»¶ä¸­çš„é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ã€‚ + notice_account_unknown_email: 未知用户 + notice_can_t_change_password: 该å¸å·ä½¿ç”¨äº†å¤–部认è¯ï¼Œå› æ­¤æ— æ³•更改密ç ã€‚ + notice_account_lost_email_sent: 系统已将引导您设置新密ç çš„邮件å‘é€ç»™æ‚¨ã€‚ + notice_account_activated: 您的å¸å·å·²è¢«æ¿€æ´»ã€‚您现在å¯ä»¥ç™»å½•了。 + notice_successful_create: 创建æˆåŠŸ + notice_successful_update: æ›´æ–°æˆåŠŸ + notice_successful_delete: 删除æˆåŠŸ + notice_successful_connection: 连接æˆåŠŸ + notice_file_not_found: 您访问的页é¢ä¸å­˜åœ¨æˆ–已被删除。 + notice_locking_conflict: æ•°æ®å·²è¢«å¦ä¸€ä½ç”¨æˆ·æ›´æ–° + notice_not_authorized: 对ä¸èµ·ï¼Œæ‚¨æ— æƒè®¿é—®æ­¤é¡µé¢ã€‚ + notice_not_authorized_archived_project: è¦è®¿é—®çš„项目已ç»å½’档。 + notice_email_sent: "邮件已å‘é€è‡³ %{value}" + notice_email_error: "å‘é€é‚®ä»¶æ—¶å‘生错误 (%{value})" + notice_feeds_access_key_reseted: 您的Atomå­˜å–键已被é‡ç½®ã€‚ + notice_api_access_key_reseted: 您的API访问键已被é‡ç½®ã€‚ + notice_failed_to_save_issues: "%{count} 个问题ä¿å­˜å¤±è´¥ï¼ˆå…±é€‰æ‹© %{total} 个问题):%{ids}." + notice_failed_to_save_members: "æˆå‘˜ä¿å­˜å¤±è´¥: %{errors}." + notice_no_issue_selected: "未选择任何问题ï¼è¯·é€‰æ‹©æ‚¨è¦ç¼–辑的问题。" + notice_account_pending: "您的å¸å·å·²è¢«æˆåŠŸåˆ›å»ºï¼Œæ­£åœ¨ç­‰å¾…ç®¡ç†å‘˜çš„审核。" + notice_default_data_loaded: æˆåŠŸè½½å…¥é»˜è®¤è®¾ç½®ã€‚ + notice_unable_delete_version: 无法删除版本 + notice_unable_delete_time_entry: 无法删除工时 + notice_issue_done_ratios_updated: 问题完æˆåº¦å·²æ›´æ–°ã€‚ + notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})" + + error_can_t_load_default_data: "无法载入默认设置:%{value}" + error_scm_not_found: "版本库中ä¸å­˜åœ¨è¯¥æ¡ç›®å’Œï¼ˆæˆ–)其修订版本。" + error_scm_command_failed: "访问版本库时å‘生错误:%{value}" + error_scm_annotate: "该æ¡ç›®ä¸å­˜åœ¨æˆ–无法追溯。" + error_issue_not_found_in_project: '问题ä¸å­˜åœ¨æˆ–ä¸å±žäºŽæ­¤é¡¹ç›®' + error_no_tracker_in_project: 该项目未设定跟踪标签,请检查项目é…置。 + error_no_default_issue_status: 未设置默认的问题状æ€ã€‚请检查系统设置("管ç†" -> "问题状æ€")。 + error_can_not_delete_custom_field: 无法删除自定义属性 + error_can_not_delete_tracker: "该跟踪标签已包å«é—®é¢˜,无法删除" + error_can_not_remove_role: "该角色正在使用中,无法删除" + error_can_not_reopen_issue_on_closed_version: 该问题被关è”到一个已ç»å…³é—­çš„ç‰ˆæœ¬ï¼Œå› æ­¤æ— æ³•é‡æ–°æ‰“开。 + error_can_not_archive_project: 该项目无法被存档 + error_issue_done_ratios_not_updated: 问题完æˆåº¦æœªèƒ½è¢«æ›´æ–°ã€‚ + error_workflow_copy_source: 请选择一个æºè·Ÿè¸ªæ ‡ç­¾æˆ–者角色 + error_workflow_copy_target: 请选择目标跟踪标签和角色 + error_unable_delete_issue_status: '无法删除问题状æ€' + error_unable_to_connect: "无法连接 (%{value})" + warning_attachments_not_saved: "%{count} 个文件ä¿å­˜å¤±è´¥" + + mail_subject_lost_password: "您的 %{value} 密ç " + mail_body_lost_password: '请点击以下链接æ¥ä¿®æ”¹æ‚¨çš„密ç ï¼š' + mail_subject_register: "%{value}å¸å·æ¿€æ´»" + mail_body_register: 'è¯·ç‚¹å‡»ä»¥ä¸‹é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ï¼š' + mail_body_account_information_external: "您å¯ä»¥ä½¿ç”¨æ‚¨çš„ %{value} å¸å·æ¥ç™»å½•。" + mail_body_account_information: 您的å¸å·ä¿¡æ¯ + mail_subject_account_activation_request: "%{value}å¸å·æ¿€æ´»è¯·æ±‚" + mail_body_account_activation_request: "新用户(%{value}ï¼‰å·²å®Œæˆæ³¨å†Œï¼Œæ­£åœ¨ç­‰å€™æ‚¨çš„审核:" + mail_subject_reminder: "%{count} 个问题需è¦å°½å¿«è§£å†³ (%{days})" + mail_body_reminder: "指派给您的 %{count} 个问题需è¦åœ¨ %{days} 天内完æˆï¼š" + mail_subject_wiki_content_added: "'%{id}' wiki页é¢å·²æ·»åŠ " + mail_body_wiki_content_added: "'%{id}' wiki页é¢å·²ç”± %{author} 添加。" + mail_subject_wiki_content_updated: "'%{id}' wiki页é¢å·²æ›´æ–°ã€‚" + mail_body_wiki_content_updated: "'%{id}' wiki页é¢å·²ç”± %{author} 更新。" + + + field_name: åç§° + field_description: æè¿° + field_summary: æ‘˜è¦ + field_is_required: å¿…å¡« + field_firstname: åå­— + field_lastname: å§“æ° + field_mail: é‚®ä»¶åœ°å€ + field_filename: 文件 + field_filesize: å¤§å° + field_downloads: 下载次数 + field_author: 作者 + field_created_on: 创建于 + field_updated_on: 更新于 + field_field_format: æ ¼å¼ + field_is_for_all: 用于所有项目 + field_possible_values: å¯èƒ½çš„值 + field_regexp: æ­£åˆ™è¡¨è¾¾å¼ + field_min_length: 最å°é•¿åº¦ + field_max_length: 最大长度 + field_value: 值 + field_category: 类别 + field_title: 标题 + field_project: 项目 + field_issue: 问题 + field_status: çŠ¶æ€ + field_notes: 说明 + field_is_closed: 已关闭的问题 + field_is_default: 默认值 + field_tracker: 跟踪 + field_subject: 主题 + field_due_date: è®¡åˆ’å®Œæˆæ—¥æœŸ + field_assigned_to: 指派给 + field_priority: 优先级 + field_fixed_version: 目标版本 + field_user: 用户 + field_principal: 用户/用户组 + field_role: 角色 + field_homepage: 主页 + field_is_public: 公开 + field_parent: 上级项目 + field_is_in_roadmap: 在路线图中显示 + field_login: 登录å + field_mail_notification: 邮件通知 + field_admin: 管ç†å‘˜ + field_last_login_on: 最åŽç™»å½• + field_language: 语言 + field_effective_date: 日期 + field_password: å¯†ç  + field_new_password: æ–°å¯†ç  + field_password_confirmation: 确认 + field_version: 版本 + field_type: 类型 + field_host: 主机 + field_port: ç«¯å£ + field_account: å¸å· + field_base_dn: Base DN + field_attr_login: 登录å属性 + field_attr_firstname: å字属性 + field_attr_lastname: å§“æ°å±žæ€§ + field_attr_mail: 邮件属性 + field_onthefly: 峿—¶ç”¨æˆ·ç”Ÿæˆ + field_start_date: 开始日期 + field_done_ratio: "% 完æˆ" + field_auth_source: è®¤è¯æ¨¡å¼ + field_hide_mail: éšè—æˆ‘çš„é‚®ä»¶åœ°å€ + field_comments: 注释 + field_url: URL + field_start_page: 起始页 + field_subproject: å­é¡¹ç›® + field_hours: å°æ—¶ + field_activity: 活动 + field_spent_on: 日期 + field_identifier: 标识 + field_is_filter: 作为过滤æ¡ä»¶ + field_issue_to: 相关问题 + field_delay: 延期 + field_assignable: é—®é¢˜å¯æŒ‡æ´¾ç»™æ­¤è§’色 + field_redirect_existing_links: é‡å®šå‘到现有链接 + field_estimated_hours: 预期时间 + field_column_names: 列 + field_time_entries: 工时 + field_time_zone: 时区 + field_searchable: å¯ç”¨ä½œæœç´¢æ¡ä»¶ + field_default_value: 默认值 + field_comments_sorting: 显示注释 + field_parent_title: ä¸Šçº§é¡µé¢ + field_editable: å¯ç¼–辑 + field_watcher: 跟踪者 + field_identity_url: OpenID URL + field_content: 内容 + field_group_by: æ ¹æ®æ­¤æ¡ä»¶åˆ†ç»„ + field_sharing: 共享 + field_parent_issue: 父任务 + field_member_of_group: 用户组的æˆå‘˜ + field_assigned_to_role: 角色的æˆå‘˜ + field_text: 文本字段 + field_visible: å¯è§çš„ + + setting_app_title: åº”ç”¨ç¨‹åºæ ‡é¢˜ + setting_app_subtitle: 应用程åºå­æ ‡é¢˜ + setting_welcome_text: 欢迎文字 + setting_default_language: 默认语言 + setting_login_required: è¦æ±‚è®¤è¯ + setting_self_registration: å…许自注册 + setting_attachment_max_size: 附件大å°é™åˆ¶ + setting_issues_export_limit: 问题导出æ¡ç›®çš„é™åˆ¶ + setting_mail_from: 邮件å‘ä»¶äººåœ°å€ + setting_bcc_recipients: ä½¿ç”¨å¯†ä»¶æŠ„é€ (bcc) + setting_plain_text_mail: 纯文本(无HTML) + setting_host_name: 主机åç§° + setting_text_formatting: æ–‡æœ¬æ ¼å¼ + setting_wiki_compression: 压缩WikiåŽ†å²æ–‡æ¡£ + setting_feeds_limit: Atom Feedå†…å®¹æ¡æ•°é™åˆ¶ + setting_default_projects_public: 新建项目默认为公开项目 + setting_autofetch_changesets: 自动获å–程åºå˜æ›´ + setting_sys_api_enabled: å¯ç”¨ç”¨äºŽç‰ˆæœ¬åº“管ç†çš„Web Service + setting_commit_ref_keywords: 用于引用问题的关键字 + setting_commit_fix_keywords: 用于解决问题的关键字 + setting_autologin: 自动登录 + setting_date_format: æ—¥æœŸæ ¼å¼ + setting_time_format: æ—¶é—´æ ¼å¼ + setting_cross_project_issue_relations: å…许ä¸åŒé¡¹ç›®ä¹‹é—´çš„é—®é¢˜å…³è” + setting_issue_list_default_columns: 问题列表中显示的默认列 + setting_emails_header: 邮件头 + setting_emails_footer: 邮件签å + setting_protocol: åè®® + setting_per_page_options: æ¯é¡µæ˜¾ç¤ºæ¡ç›®ä¸ªæ•°çš„设置 + setting_user_format: ç”¨æˆ·æ˜¾ç¤ºæ ¼å¼ + setting_activity_days_default: 在项目活动中显示的天数 + setting_display_subprojects_issues: 在项目页é¢ä¸Šé»˜è®¤æ˜¾ç¤ºå­é¡¹ç›®çš„问题 + setting_enabled_scm: å¯ç”¨ SCM + setting_mail_handler_body_delimiters: åœ¨è¿™äº›è¡Œä¹‹åŽæˆªæ–­é‚®ä»¶ + setting_mail_handler_api_enabled: å¯ç”¨ç”¨äºŽæŽ¥æ”¶é‚®ä»¶çš„æœåŠ¡ + setting_mail_handler_api_key: API key + setting_sequential_project_identifiers: 顺åºäº§ç”Ÿé¡¹ç›®æ ‡è¯† + setting_gravatar_enabled: 使用Gravatarç”¨æˆ·å¤´åƒ + setting_gravatar_default: 默认的Gravatarå¤´åƒ + setting_diff_max_lines_displayed: 查看差别页é¢ä¸Šæ˜¾ç¤ºçš„æœ€å¤§è¡Œæ•° + setting_file_max_size_displayed: å…许直接显示的最大文本文件 + setting_repository_log_display_limit: åœ¨æ–‡ä»¶å˜æ›´è®°å½•页é¢ä¸Šæ˜¾ç¤ºçš„æœ€å¤§ä¿®è®¢ç‰ˆæœ¬æ•°é‡ + setting_openid: å…许使用OpenID登录和注册 + setting_password_min_length: 最短密ç é•¿åº¦ + setting_new_project_user_role_id: éžç®¡ç†å‘˜ç”¨æˆ·æ–°å»ºé¡¹ç›®æ—¶å°†è¢«èµ‹äºˆçš„(在该项目中的)角色 + setting_default_projects_modules: 新建项目默认å¯ç”¨çš„æ¨¡å— + setting_issue_done_ratio: 计算问题完æˆåº¦ï¼š + setting_issue_done_ratio_issue_field: 使用问题(的完æˆåº¦ï¼‰å±žæ€§ + setting_issue_done_ratio_issue_status: ä½¿ç”¨é—®é¢˜çŠ¶æ€ + setting_start_of_week: 日历开始于 + setting_rest_api_enabled: å¯ç”¨REST web service + setting_cache_formatted_text: 缓存格å¼åŒ–文字 + setting_default_notification_option: 默认æé†’选项 + setting_commit_logtime_enabled: 激活时间日志 + setting_commit_logtime_activity_id: 记录的活动 + setting_gantt_items_limit: 在甘特图上显示的最大记录数 + + permission_add_project: 新建项目 + permission_add_subprojects: 新建å­é¡¹ç›® + permission_edit_project: 编辑项目 + permission_select_project_modules: é€‰æ‹©é¡¹ç›®æ¨¡å— + permission_manage_members: ç®¡ç†æˆå‘˜ + permission_manage_project_activities: 管ç†é¡¹ç›®æ´»åЍ + permission_manage_versions: 管ç†ç‰ˆæœ¬ + permission_manage_categories: 管ç†é—®é¢˜ç±»åˆ« + permission_view_issues: 查看问题 + permission_add_issues: 新建问题 + permission_edit_issues: 更新问题 + permission_manage_issue_relations: 管ç†é—®é¢˜å…³è” + permission_add_issue_notes: 添加说明 + permission_edit_issue_notes: 编辑说明 + permission_edit_own_issue_notes: 编辑自己的说明 + permission_move_issues: 移动问题 + permission_delete_issues: 删除问题 + permission_manage_public_queries: 管ç†å…¬å¼€çš„æŸ¥è¯¢ + permission_save_queries: ä¿å­˜æŸ¥è¯¢ + permission_view_gantt: 查看甘特图 + permission_view_calendar: 查看日历 + permission_view_issue_watchers: 查看跟踪者列表 + permission_add_issue_watchers: 添加跟踪者 + permission_delete_issue_watchers: 删除跟踪者 + permission_log_time: 登记工时 + permission_view_time_entries: 查看耗时 + permission_edit_time_entries: 编辑耗时 + permission_edit_own_time_entries: 编辑自己的耗时 + permission_manage_news: ç®¡ç†æ–°é—» + permission_comment_news: 为新闻添加评论 + permission_view_documents: 查看文档 + permission_manage_files: ç®¡ç†æ–‡ä»¶ + permission_view_files: 查看文件 + permission_manage_wiki: 管ç†Wiki + permission_rename_wiki_pages: é‡å®šå‘/é‡å‘½åWikié¡µé¢ + permission_delete_wiki_pages: 删除Wikié¡µé¢ + permission_view_wiki_pages: 查看Wiki + permission_view_wiki_edits: 查看Wiki历å²è®°å½• + permission_edit_wiki_pages: 编辑Wikié¡µé¢ + permission_delete_wiki_pages_attachments: 删除附件 + permission_protect_wiki_pages: ä¿æŠ¤Wikié¡µé¢ + permission_manage_repository: 管ç†ç‰ˆæœ¬åº“ + permission_browse_repository: æµè§ˆç‰ˆæœ¬åº“ + permission_view_changesets: æŸ¥çœ‹å˜æ›´ + permission_commit_access: 访问æäº¤ä¿¡æ¯ + permission_manage_boards: 管ç†è®¨è®ºåŒº + permission_view_messages: æŸ¥çœ‹å¸–å­ + permission_add_messages: å‘è¡¨å¸–å­ + permission_edit_messages: ç¼–è¾‘å¸–å­ + permission_edit_own_messages: ç¼–è¾‘è‡ªå·±çš„å¸–å­ + permission_delete_messages: åˆ é™¤å¸–å­ + permission_delete_own_messages: åˆ é™¤è‡ªå·±çš„å¸–å­ + permission_export_wiki_pages: 导出 wiki é¡µé¢ + permission_manage_subtasks: 管ç†å­ä»»åŠ¡ + + project_module_issue_tracking: 问题跟踪 + project_module_time_tracking: 时间跟踪 + project_module_news: æ–°é—» + project_module_documents: 文档 + project_module_files: 文件 + project_module_wiki: Wiki + project_module_repository: 版本库 + project_module_boards: 讨论区 + project_module_calendar: 日历 + project_module_gantt: 甘特图 + + label_user: 用户 + label_user_plural: 用户 + label_user_new: 新建用户 + label_user_anonymous: 匿å用户 + label_project: 项目 + label_project_new: 新建项目 + label_project_plural: 项目 + label_x_projects: + zero: 无项目 + one: 1 个项目 + other: "%{count} 个项目" + label_project_all: 所有的项目 + label_project_latest: 最近的项目 + label_issue: 问题 + label_issue_new: 新建问题 + label_issue_plural: 问题 + label_issue_view_all: 查看所有问题 + label_issues_by: "按 %{value} 分组显示问题" + label_issue_added: 问题已添加 + label_issue_updated: 问题已更新 + label_document: 文档 + label_document_new: 新建文档 + label_document_plural: 文档 + label_document_added: 文档已添加 + label_role: 角色 + label_role_plural: 角色 + label_role_new: 新建角色 + label_role_and_permissions: 角色和æƒé™ + label_member: æˆå‘˜ + label_member_new: 新建æˆå‘˜ + label_member_plural: æˆå‘˜ + label_tracker: 跟踪标签 + label_tracker_plural: 跟踪标签 + label_tracker_new: 新建跟踪标签 + label_workflow: 工作æµç¨‹ + label_issue_status: é—®é¢˜çŠ¶æ€ + label_issue_status_plural: é—®é¢˜çŠ¶æ€ + label_issue_status_new: æ–°å»ºé—®é¢˜çŠ¶æ€ + label_issue_category: 问题类别 + label_issue_category_plural: 问题类别 + label_issue_category_new: 新建问题类别 + label_custom_field: 自定义属性 + label_custom_field_plural: 自定义属性 + label_custom_field_new: 新建自定义属性 + label_enumerations: 枚举值 + label_enumeration_new: 新建枚举值 + label_information: ä¿¡æ¯ + label_information_plural: ä¿¡æ¯ + label_please_login: 请登录 + label_register: 注册 + label_login_with_open_id_option: 或使用OpenID登录 + label_password_lost: å¿˜è®°å¯†ç  + label_home: 主页 + label_my_page: æˆ‘çš„å·¥ä½œå° + label_my_account: 我的å¸å· + label_my_projects: 我的项目 + label_my_page_block: æˆ‘çš„å·¥ä½œå°æ¨¡å— + label_administration: ç®¡ç† + label_login: 登录 + label_logout: 退出 + label_help: 帮助 + label_reported_issues: 已报告的问题 + label_assigned_to_me_issues: 指派给我的问题 + label_last_login: 最åŽç™»å½• + label_registered_on: 注册于 + label_activity: 活动 + label_overall_activity: 活动概览 + label_user_activity: "%{value} 的活动" + label_new: 新建 + label_logged_as: 登录为 + label_environment: 环境 + label_authentication: è®¤è¯ + label_auth_source: è®¤è¯æ¨¡å¼ + label_auth_source_new: æ–°å»ºè®¤è¯æ¨¡å¼ + label_auth_source_plural: è®¤è¯æ¨¡å¼ + label_subproject_plural: å­é¡¹ç›® + label_subproject_new: 新建å­é¡¹ç›® + label_and_its_subprojects: "%{value} åŠå…¶å­é¡¹ç›®" + label_min_max_length: æœ€å° - 最大 长度 + label_list: 列表 + label_date: 日期 + label_integer: æ•´æ•° + label_float: 浮点数 + label_boolean: 布尔值 + label_string: 字符串 + label_text: 文本 + label_attribute: 属性 + label_attribute_plural: 属性 + label_no_data: 没有任何数æ®å¯ä¾›æ˜¾ç¤º + label_change_status: å˜æ›´çŠ¶æ€ + label_history: 历å²è®°å½• + label_attachment: 文件 + label_attachment_new: 新建文件 + label_attachment_delete: 删除文件 + label_attachment_plural: 文件 + label_file_added: 文件已添加 + label_report: 报表 + label_report_plural: 报表 + label_news: æ–°é—» + label_news_new: 添加新闻 + label_news_plural: æ–°é—» + label_news_latest: 最近的新闻 + label_news_view_all: 查看所有新闻 + label_news_added: 新闻已添加 + label_settings: é…ç½® + label_overview: 概述 + label_version: 版本 + label_version_new: 新建版本 + label_version_plural: 版本 + label_close_versions: 关闭已完æˆçš„版本 + label_confirmation: 确认 + label_export_to: 导出 + label_read: 读å–... + label_public_projects: 公开的项目 + label_open_issues: 打开 + label_open_issues_plural: 打开 + label_closed_issues: 已关闭 + label_closed_issues_plural: 已关闭 + label_x_open_issues_abbr_on_total: + zero: 0 打开 / %{total} + one: 1 打开 / %{total} + other: "%{count} 打开 / %{total}" + label_x_open_issues_abbr: + zero: 0 打开 + one: 1 打开 + other: "%{count} 打开" + label_x_closed_issues_abbr: + zero: 0 已关闭 + one: 1 已关闭 + other: "%{count} 已关闭" + label_total: åˆè®¡ + label_permissions: æƒé™ + label_current_status: 当å‰çŠ¶æ€ + label_new_statuses_allowed: å…è®¸çš„æ–°çŠ¶æ€ + label_all: 全部 + label_none: æ—  + label_nobody: 无人 + label_next: 下一页 + label_previous: 上一页 + label_used_by: 使用中 + label_details: 详情 + label_add_note: 添加说明 + label_per_page: æ¯é¡µ + label_calendar: 日历 + label_months_from: ä¸ªæœˆä»¥æ¥ + label_gantt: 甘特图 + label_internal: 内部 + label_last_changes: "最近的 %{count} æ¬¡å˜æ›´" + label_change_view_all: æŸ¥çœ‹æ‰€æœ‰å˜æ›´ + label_personalize_page: 个性化定制本页 + label_comment: 评论 + label_comment_plural: 评论 + label_x_comments: + zero: 无评论 + one: 1 æ¡è¯„论 + other: "%{count} æ¡è¯„论" + label_comment_add: 添加评论 + label_comment_added: 评论已添加 + label_comment_delete: 删除评论 + label_query: 自定义查询 + label_query_plural: 自定义查询 + label_query_new: 新建查询 + label_filter_add: 增加过滤器 + label_filter_plural: 过滤器 + label_equals: 等于 + label_not_equals: ä¸ç­‰äºŽ + label_in_less_than: 剩余天数å°äºŽ + label_in_more_than: 剩余天数大于 + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: 剩余天数 + label_today: 今天 + label_all_time: 全部时间 + label_yesterday: 昨天 + label_this_week: 本周 + label_last_week: 上周 + label_last_n_days: "æœ€åŽ %{count} 天" + label_this_month: 本月 + label_last_month: 上月 + label_this_year: 今年 + label_date_range: 日期范围 + label_less_than_ago: 之å‰å¤©æ•°å°‘于 + label_more_than_ago: 之å‰å¤©æ•°å¤§äºŽ + label_ago: 之å‰å¤©æ•° + label_contains: åŒ…å« + label_not_contains: ä¸åŒ…å« + label_day_plural: 天 + label_repository: 版本库 + label_repository_plural: 版本库 + label_browse: æµè§ˆ + label_branch: 分支 + label_tag: 标签 + label_revision: 修订 + label_revision_plural: 修订 + label_revision_id: 修订 %{value} + label_associated_revisions: 相关修订版本 + label_added: 已添加 + label_modified: 已修改 + label_copied: å·²å¤åˆ¶ + label_renamed: å·²é‡å‘½å + label_deleted: 已删除 + label_latest_revision: 最近的修订版本 + label_latest_revision_plural: 最近的修订版本 + label_view_revisions: 查看修订 + label_view_all_revisions: 查看所有修订 + label_max_size: 最大尺寸 + label_sort_highest: 置顶 + label_sort_higher: 上移 + label_sort_lower: 下移 + label_sort_lowest: 置底 + label_roadmap: 路线图 + label_roadmap_due_in: "截止日期到 %{value}" + label_roadmap_overdue: "%{value} 延期" + label_roadmap_no_issues: 该版本没有问题 + label_search: æœç´¢ + label_result_plural: 结果 + label_all_words: 所有å•è¯ + label_wiki: Wiki + label_wiki_edit: Wiki 编辑 + label_wiki_edit_plural: Wiki 编辑记录 + label_wiki_page: Wiki é¡µé¢ + label_wiki_page_plural: Wiki é¡µé¢ + label_index_by_title: 按标题索引 + label_index_by_date: 按日期索引 + label_current_version: 当å‰ç‰ˆæœ¬ + label_preview: 预览 + label_feed_plural: Feeds + label_changes_details: æ‰€æœ‰å˜æ›´çš„详情 + label_issue_tracking: 问题跟踪 + label_spent_time: 耗时 + label_overall_spent_time: 总体耗时 + label_f_hour: "%{value} å°æ—¶" + label_f_hour_plural: "%{value} å°æ—¶" + label_time_tracking: 时间跟踪 + label_change_plural: å˜æ›´ + label_statistics: 统计 + label_commits_per_month: æ¯æœˆæäº¤æ¬¡æ•° + label_commits_per_author: æ¯ç”¨æˆ·æäº¤æ¬¡æ•° + label_view_diff: 查看差别 + label_diff_inline: 直列 + label_diff_side_by_side: 并排 + label_options: 选项 + label_copy_workflow_from: 从以下选项å¤åˆ¶å·¥ä½œæµç¨‹ + label_permissions_report: æƒé™æŠ¥è¡¨ + label_watched_issues: 跟踪的问题 + label_related_issues: 相关的问题 + label_applied_status: 应用åŽçš„çŠ¶æ€ + label_loading: 载入中... + label_relation_new: æ–°å»ºå…³è” + label_relation_delete: åˆ é™¤å…³è” + label_relates_to: å…³è”到 + label_duplicates: é‡å¤ + label_duplicated_by: 与其é‡å¤ + label_blocks: 阻挡 + label_blocked_by: 被阻挡 + label_precedes: 优先于 + label_follows: è·ŸéšäºŽ + label_end_to_start: 结æŸ-开始 + label_end_to_end: 结æŸ-ç»“æŸ + label_start_to_start: 开始-开始 + label_start_to_end: 开始-ç»“æŸ + label_stay_logged_in: ä¿æŒç™»å½•çŠ¶æ€ + label_disabled: ç¦ç”¨ + label_show_completed_versions: 显示已完æˆçš„版本 + label_me: 我 + label_board: 讨论区 + label_board_new: 新建讨论区 + label_board_plural: 讨论区 + label_board_locked: é”定 + label_board_sticky: 置顶 + label_topic_plural: 主题 + label_message_plural: å¸–å­ + label_message_last: æœ€æ–°çš„å¸–å­ + label_message_new: æ–°è´´ + label_message_posted: å‘帖æˆåŠŸ + label_reply_plural: å›žå¤ + label_send_information: 给用户å‘é€å¸å·ä¿¡æ¯ + label_year: å¹´ + label_month: 月 + label_week: 周 + label_date_from: 从 + label_date_to: 到 + label_language_based: æ ¹æ®ç”¨æˆ·çš„语言 + label_sort_by: "æ ¹æ® %{value} 排åº" + label_send_test_email: å‘逿µ‹è¯•邮件 + label_feeds_access_key: Atomå­˜å–é”® + label_missing_feeds_access_key: 缺少Atomå­˜å–é”® + label_feeds_access_key_created_on: "Atomå­˜å–键是在 %{value} 之å‰å»ºç«‹çš„" + label_module_plural: æ¨¡å— + label_added_time_by: "ç”± %{author} 在 %{age} 之剿·»åŠ " + label_updated_time: " 更新于 %{value} 之å‰" + label_updated_time_by: "ç”± %{author} 更新于 %{age} 之å‰" + label_jump_to_a_project: 选择一个项目... + label_file_plural: 文件 + label_changeset_plural: å˜æ›´ + label_default_columns: 默认列 + label_no_change_option: (ä¸å˜) + label_bulk_edit_selected_issues: 批é‡ä¿®æ”¹é€‰ä¸­çš„问题 + label_theme: 主题 + label_default: 默认 + label_search_titles_only: 仅在标题中æœç´¢ + label_user_mail_option_all: "æ”¶å–æˆ‘的项目的所有通知" + label_user_mail_option_selected: "æ”¶å–选中项目的所有通知..." + label_user_mail_option_none: "䏿”¶å–任何通知" + label_user_mail_option_only_my_events: "åªæ”¶å–我跟踪或å‚与的项目的通知" + label_user_mail_option_only_assigned: "åªæ”¶å–分é…给我的" + label_user_mail_option_only_owner: åªæ”¶å–由我创建的 + label_user_mail_no_self_notified: "ä¸è¦å‘é€å¯¹æˆ‘自己æäº¤çš„修改的通知" + label_registration_activation_by_email: é€šè¿‡é‚®ä»¶è®¤è¯æ¿€æ´»å¸å· + label_registration_manual_activation: 手动激活å¸å· + label_registration_automatic_activation: 自动激活å¸å· + label_display_per_page: "æ¯é¡µæ˜¾ç¤ºï¼š%{value}" + label_age: æäº¤æ—¶é—´ + label_change_properties: 修改属性 + label_general: 一般 + label_more: 更多 + label_scm: SCM + label_plugins: æ’ä»¶ + label_ldap_authentication: LDAP è®¤è¯ + label_downloads_abbr: D/L + label_optional_description: å¯é€‰çš„æè¿° + label_add_another_file: 添加其它文件 + label_preferences: 首选项 + label_chronological_order: æŒ‰æ—¶é—´é¡ºåº + label_reverse_chronological_order: 按时间顺åºï¼ˆå€’åºï¼‰ + label_planning: 计划 + label_incoming_emails: 接收邮件 + label_generate_key: 生æˆä¸€ä¸ªkey + label_issue_watchers: 跟踪者 + label_example: 示例 + label_display: 显示 + label_sort: æŽ’åº + label_ascending: å‡åº + label_descending: é™åº + label_date_from_to: 从 %{start} 到 %{end} + label_wiki_content_added: Wiki 页é¢å·²æ·»åŠ  + label_wiki_content_updated: Wiki 页é¢å·²æ›´æ–° + label_group: 组 + label_group_plural: 组 + label_group_new: 新建组 + label_time_entry_plural: 耗时 + label_version_sharing_none: ä¸å…±äº« + label_version_sharing_descendants: 与å­é¡¹ç›®å…±äº« + label_version_sharing_hierarchy: 与项目继承层次共享 + label_version_sharing_tree: 与项目树共享 + label_version_sharing_system: 与所有项目共享 + label_update_issue_done_ratios: 更新问题的完æˆåº¦ + label_copy_source: æº + label_copy_target: 目标 + label_copy_same_as_target: 与目标一致 + label_display_used_statuses_only: åªæ˜¾ç¤ºè¢«æ­¤è·Ÿè¸ªæ ‡ç­¾ä½¿ç”¨çš„çŠ¶æ€ + label_api_access_key: API访问键 + label_missing_api_access_key: 缺少API访问键 + label_api_access_key_created_on: API访问键是在 %{value} 之å‰å»ºç«‹çš„ + label_profile: 简介 + label_subtask_plural: å­ä»»åŠ¡ + label_project_copy_notifications: å¤åˆ¶é¡¹ç›®æ—¶å‘é€é‚®ä»¶é€šçŸ¥ + label_principal_search: "æœç´¢ç”¨æˆ·æˆ–组:" + label_user_search: "æœç´¢ç”¨æˆ·ï¼š" + + button_login: 登录 + button_submit: æäº¤ + button_save: ä¿å­˜ + button_check_all: 全选 + button_uncheck_all: 清除 + button_delete: 删除 + button_create: 创建 + button_create_and_continue: 创建并继续 + button_test: 测试 + button_edit: 编辑 + button_edit_associated_wikipage: "编辑相关wiki页é¢: %{page_title}" + button_add: 新增 + button_change: 修改 + button_apply: 应用 + button_clear: 清除 + button_lock: é”定 + button_unlock: è§£é” + button_download: 下载 + button_list: 列表 + button_view: 查看 + button_move: 移动 + button_move_and_follow: 移动并转到新问题 + button_back: 返回 + button_cancel: å–æ¶ˆ + button_activate: 激活 + button_sort: æŽ’åº + button_log_time: 登记工时 + button_rollback: æ¢å¤åˆ°è¿™ä¸ªç‰ˆæœ¬ + button_watch: 跟踪 + button_unwatch: å–æ¶ˆè·Ÿè¸ª + button_reply: å›žå¤ + button_archive: 存档 + button_unarchive: å–æ¶ˆå­˜æ¡£ + button_reset: é‡ç½® + button_rename: é‡å‘½å/é‡å®šå‘ + button_change_password: ä¿®æ”¹å¯†ç  + button_copy: å¤åˆ¶ + button_copy_and_follow: å¤åˆ¶å¹¶è½¬åˆ°æ–°é—®é¢˜ + button_annotate: 追溯 + button_update: æ›´æ–° + button_configure: é…ç½® + button_quote: 引用 + button_duplicate: 副本 + button_show: 显示 + + status_active: 活动的 + status_registered: 已注册 + status_locked: å·²é”定 + + version_status_open: 打开 + version_status_locked: é”定 + version_status_closed: 关闭 + + field_active: 活动 + + text_select_mail_notifications: 选择需è¦å‘é€é‚®ä»¶é€šçŸ¥çš„动作 + text_regexp_info: 例如:^[A-Z0-9]+$ + text_min_max_length_info: 0 表示没有é™åˆ¶ + text_project_destroy_confirmation: 您确信è¦åˆ é™¤è¿™ä¸ªé¡¹ç›®ä»¥åŠæ‰€æœ‰ç›¸å…³çš„æ•°æ®å—? + text_subprojects_destroy_warning: "以下å­é¡¹ç›®ä¹Ÿå°†è¢«åŒæ—¶åˆ é™¤ï¼š%{value}" + text_workflow_edit: 选择角色和跟踪标签æ¥ç¼–辑工作æµç¨‹ + text_are_you_sure: 您确定? + text_journal_changed: "%{label} 从 %{old} å˜æ›´ä¸º %{new}" + text_journal_set_to: "%{label} 被设置为 %{value}" + text_journal_deleted: "%{label} 已删除 (%{old})" + text_journal_added: "%{label} %{value} 已添加" + text_tip_issue_begin_day: 今天开始的任务 + text_tip_issue_end_day: 今天结æŸçš„任务 + text_tip_issue_begin_end_day: 今天开始并结æŸçš„任务 + text_caracters_maximum: "最多 %{count} 个字符。" + text_caracters_minimum: "è‡³å°‘éœ€è¦ %{count} 个字符。" + text_length_between: "长度必须在 %{min} 到 %{max} 个字符之间。" + text_tracker_no_workflow: 此跟踪标签未定义工作æµç¨‹ + text_unallowed_characters: éžæ³•字符 + text_comma_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆç”¨é€—å·,分开)。 + text_line_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆæ¯è¡Œä¸€ä¸ªå€¼ï¼‰ã€‚ + text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸­å¼•用和解决问题 + text_issue_added: "问题 %{id} 已由 %{author} æäº¤ã€‚" + text_issue_updated: "问题 %{id} 已由 %{author} 更新。" + text_wiki_destroy_confirmation: 您确定è¦åˆ é™¤è¿™ä¸ª wiki åŠå…¶æ‰€æœ‰å†…容å—? + text_issue_category_destroy_question: "有一些问题(%{count} ä¸ªï¼‰å±žäºŽæ­¤ç±»åˆ«ã€‚æ‚¨æƒ³è¿›è¡Œå“ªç§æ“作?" + text_issue_category_destroy_assignments: 删除问题的所属类别(问题å˜ä¸ºæ— ç±»åˆ«ï¼‰ + text_issue_category_reassign_to: 为问题选择其它类别 + text_user_mail_option: "对于没有选中的项目,您将åªä¼šæ”¶åˆ°æ‚¨è·Ÿè¸ªæˆ–å‚与的项目的通知(比如说,您是问题的报告者, 或被指派解决此问题)。" + text_no_configuration_data: "角色ã€è·Ÿè¸ªæ ‡ç­¾ã€é—®é¢˜çжæ€å’Œå·¥ä½œæµç¨‹è¿˜æ²¡æœ‰è®¾ç½®ã€‚\n强烈建议您先载入默认设置,然åŽåœ¨æ­¤åŸºç¡€ä¸Šè¿›è¡Œä¿®æ”¹ã€‚" + text_load_default_configuration: 载入默认设置 + text_status_changed_by_changeset: "å·²åº”ç”¨åˆ°å˜æ›´åˆ—表 %{value}." + text_time_logged_by_changeset: "已应用到修订版本 %{value}." + text_issues_destroy_confirmation: '您确定è¦åˆ é™¤é€‰ä¸­çš„问题å—?' + text_select_project_modules: '请选择此项目å¯ä»¥ä½¿ç”¨çš„æ¨¡å—:' + text_default_administrator_account_changed: 默认的管ç†å‘˜å¸å·å·²æ”¹å˜ + text_file_repository_writable: 附件路径å¯å†™ + text_plugin_assets_writable: æ’件的附件路径å¯å†™ + text_rmagick_available: RMagick å¯ç”¨ï¼ˆå¯é€‰çš„) + text_destroy_time_entries_question: 您è¦åˆ é™¤çš„问题已ç»ä¸ŠæŠ¥äº† %{hours} å°æ—¶çš„工作é‡ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? + text_destroy_time_entries: åˆ é™¤ä¸ŠæŠ¥çš„å·¥ä½œé‡ + text_assign_time_entries_to_project: å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æäº¤åˆ°é¡¹ç›®ä¸­ + text_reassign_time_entries: 'å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æŒ‡å®šåˆ°æ­¤é—®é¢˜ï¼š' + text_user_wrote: "%{value} 写到:" + text_enumeration_destroy_question: "%{count} 个对象被关è”到了这个枚举值。" + text_enumeration_category_reassign_to: '将它们关è”到新的枚举值:' + text_email_delivery_not_configured: "邮件傿•°å°šæœªé…置,因此邮件通知功能已被ç¦ç”¨ã€‚\n请在config/configuration.yml中é…置您的SMTPæœåŠ¡å™¨ä¿¡æ¯å¹¶é‡æ–°å¯åŠ¨ä»¥ä½¿å…¶ç”Ÿæ•ˆã€‚" + text_repository_usernames_mapping: "选择或更新与版本库中的用户å对应的Redmine用户。\n版本库中与Redmine中的åŒå用户将被自动对应。" + text_diff_truncated: '... å·®åˆ«å†…å®¹è¶…è¿‡äº†å¯æ˜¾ç¤ºçš„æœ€å¤§è¡Œæ•°å¹¶å·²è¢«æˆªæ–­' + text_custom_field_possible_values_info: 'æ¯é¡¹æ•°å€¼ä¸€è¡Œ' + text_wiki_page_destroy_question: æ­¤é¡µé¢æœ‰ %{descendants} 个å­é¡µé¢å’Œä¸‹çº§é¡µé¢ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? + text_wiki_page_nullify_children: å°†å­é¡µé¢ä¿ç•™ä¸ºæ ¹é¡µé¢ + text_wiki_page_destroy_children: 删除å­é¡µé¢åŠå…¶æ‰€æœ‰ä¸‹çº§é¡µé¢ + text_wiki_page_reassign_children: å°†å­é¡µé¢çš„上级页é¢è®¾ç½®ä¸º + text_own_membership_delete_confirmation: 你正在删除你现有的æŸäº›æˆ–全部æƒé™ï¼Œå¦‚果这样åšäº†ä½ å¯èƒ½å°†ä¼šå†ä¹Ÿæ— æ³•编辑该项目了。你确定è¦ç»§ç»­å—? + text_zoom_in: 放大 + text_zoom_out: ç¼©å° + + default_role_manager: 管ç†äººå‘˜ + default_role_developer: å¼€å‘人员 + default_role_reporter: 报告人员 + default_tracker_bug: 错误 + default_tracker_feature: 功能 + default_tracker_support: æ”¯æŒ + default_issue_status_new: 新建 + default_issue_status_in_progress: 进行中 + default_issue_status_resolved: 已解决 + default_issue_status_feedback: å馈 + default_issue_status_closed: 已关闭 + default_issue_status_rejected: å·²æ‹’ç» + default_doc_category_user: 用户文档 + default_doc_category_tech: 技术文档 + default_priority_low: 低 + default_priority_normal: 普通 + default_priority_high: 高 + default_priority_urgent: 紧急 + default_priority_immediate: 立刻 + default_activity_design: 设计 + default_activity_development: å¼€å‘ + + enumeration_issue_priorities: 问题优先级 + enumeration_doc_categories: 文档类别 + enumeration_activities: 活动(时间跟踪) + enumeration_system_activity: 系统活动 + + field_warn_on_leaving_unsaved: 当离开未ä¿å­˜å†…å®¹çš„é¡µé¢æ—¶ï¼Œæç¤ºæˆ‘ + text_warn_on_leaving_unsaved: 若离开当å‰é¡µé¢ï¼Œåˆ™è¯¥é¡µé¢å†…未ä¿å­˜çš„内容将丢失。 + label_my_queries: 我的自定义查询 + text_journal_changed_no_detail: "%{label} 已更新。" + label_news_comment_added: 添加到新闻的评论 + button_expand_all: 展开所有 + button_collapse_all: åˆæ‹¢æ‰€æœ‰ + label_additional_workflow_transitions_for_assignee: 当用户是问题的分é…对象时所å…许的问题状æ€è½¬æ¢ + label_additional_workflow_transitions_for_author: 当用户是问题作者时所å…许的问题状æ€è½¬æ¢ + label_bulk_edit_selected_time_entries: 批é‡ä¿®æ”¹é€‰å®šçš„æ—¶é—´æ¡ç›® + text_time_entries_destroy_confirmation: 是å¦ç¡®å®šè¦åˆ é™¤é€‰å®šçš„æ—¶é—´æ¡ç›®ï¼Ÿ + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: 问题备注已添加 + label_issue_status_updated: é—®é¢˜çŠ¶æ€æ›´æ–° + label_issue_priority_updated: 问题优先级更新 + label_issues_visibility_own: 创建或分é…给用户的问题 + field_issues_visibility: 问题å¯è§ + label_issues_visibility_all: 全部问题 + permission_set_own_issues_private: è®¾ç½®è‡ªå·±çš„é—®é¢˜ä¸ºå…¬å¼€æˆ–ç§æœ‰ + field_is_private: ç§æœ‰ + permission_set_issues_private: è®¾ç½®é—®é¢˜ä¸ºå…¬å¼€æˆ–ç§æœ‰ + label_issues_visibility_public: 全部éžç§æœ‰é—®é¢˜ + text_issues_destroy_descendants_confirmation: æ­¤æ“ä½œåŒæ—¶ä¼šåˆ é™¤ %{count} 个å­ä»»åŠ¡ã€‚ + + field_commit_logs_encoding: æäº¤æ³¨é‡Šçš„ç¼–ç  + field_scm_path_encoding: è·¯å¾„ç¼–ç  + text_scm_path_encoding_note: "默认: UTF-8" + field_path_to_repository: 库路径 + field_root_directory: 根目录 + field_cvs_module: CVS Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: 本地库 (e.g. /hgrepo, c:\hgrepo) + text_scm_command: 命令 + text_scm_command_version: 版本 + label_git_report_last_commit: 报告最åŽä¸€æ¬¡æ–‡ä»¶/目录æäº¤ + text_scm_config: 您å¯ä»¥åœ¨config/configuration.yml中é…置您的SCM命令。 请在编辑åŽï¼Œé‡å¯Redmine应用。 + text_scm_command_not_available: Scm命令ä¸å¯ç”¨ã€‚ 请检查管ç†é¢æ¿çš„é…置。 + text_git_repository_note: 库中无内容。(e.g. /gitrepo, c:\gitrepo) + notice_issue_successful_create: 问题 %{id} 已创建。 + label_between: 介于 + setting_issue_group_assignment: å…许问题被分é…给组 + label_diff: diff + description_query_sort_criteria_direction: æŽ’åºæ–¹å¼ + description_project_scope: æœç´¢èŒƒå›´ + description_filter: 过滤器 + description_user_mail_notification: 邮件通知设置 + description_date_from: 输入开始日期 + description_message_content: ä¿¡æ¯å†…容 + description_available_columns: 备选列 + description_date_range_interval: æŒ‰å¼€å§‹æ—¥æœŸå’Œç»“æŸæ—¥æœŸé€‰æ‹©èŒƒå›´ + description_issue_category_reassign: 选择问题类别 + description_search: æœç´¢å­—段 + description_notes: 批注 + description_date_range_list: 从列表中选择范围 + description_choose_project: 项目 + description_date_to: è¾“å…¥ç»“æŸæ—¥æœŸ + description_query_sort_criteria_attribute: æŽ’åºæ–¹å¼ + description_wiki_subpages_reassign: é€‰æ‹©çˆ¶é¡µé¢ + description_selected_columns: 已选列 + label_parent_revision: 父修订 + label_child_revision: å­ä¿®è®¢ + error_scm_annotate_big_text_file: 输入文本内容超长,无法输入。 + setting_default_issue_start_date_to_creation_date: ä½¿ç”¨å½“å‰æ—¥æœŸä½œä¸ºæ–°é—®é¢˜çš„开始日期 + button_edit_section: 编辑此区域 + setting_repositories_encodings: é™„ä»¶å’Œç‰ˆæœ¬åº“ç¼–ç  + description_all_columns: 所有列 + button_export: 导出 + label_export_options: "%{export_format} 导出选项" + error_attachment_too_big: 该文件无法上传。超过文件大å°é™åˆ¶ (%{max_size}) + notice_failed_to_save_time_entries: "无法ä¿å­˜ä¸‹åˆ—所选å–çš„ %{total} 个项目中的 %{count} 工时: %{ids}。" + label_x_issues: + zero: 0 问题 + one: 1 问题 + other: "%{count} 问题" + label_repository_new: 新建版本库 + field_repository_is_default: 主版本库 + label_copy_attachments: å¤åˆ¶é™„ä»¶ + label_item_position: "%{position}/%{count}" + label_completed_versions: 已完æˆçš„版本 + text_project_identifier_info: ä»…å°å†™å­—æ¯ï¼ˆa-zï¼‰ã€æ•°å­—ã€ç ´æŠ˜å·ï¼ˆ-)和下划线(_)å¯ä»¥ä½¿ç”¨ã€‚
一旦ä¿å­˜ï¼Œæ ‡è¯†æ— æ³•修改。 + 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/0f/0f05e49d9c9481ce6698da320e7a0f815ec9a184.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0f/0f05e49d9c9481ce6698da320e7a0f815ec9a184.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,456 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) +require 'digest/md5' + +class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase + + def setup + @formatter = Redmine::WikiFormatting::Textile::Formatter + end + + MODIFIERS = { + "*" => 'strong', # bold + "_" => 'em', # italic + "+" => 'ins', # underline + "-" => 'del', # deleted + "^" => 'sup', # superscript + "~" => 'sub' # subscript + } + + def test_modifiers + assert_html_output( + '*bold*' => 'bold', + 'before *bold*' => 'before bold', + '*bold* after' => 'bold after', + '*two words*' => 'two words', + '*two*words*' => 'two*words', + '*two * words*' => 'two * words', + '*two* *words*' => 'two words', + '*(two)* *(words)*' => '(two) (words)', + # with class + '*(foo)two words*' => 'two words' + ) + end + + def test_modifiers_combination + MODIFIERS.each do |m1, tag1| + MODIFIERS.each do |m2, tag2| + next if m1 == m2 + text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}" + html = "<#{tag2}><#{tag1}>Phrase modifiers" + assert_html_output text => html + end + end + end + + def test_styles + # single style + assert_html_output({ + 'p{color:red}. text' => '

text

', + 'p{color:red;}. text' => '

text

', + 'p{color: red}. text' => '

text

', + 'p{color:#f00}. text' => '

text

', + 'p{color:#ff0000}. text' => '

text

', + 'p{border:10px}. text' => '

text

', + 'p{border:10}. text' => '

text

', + 'p{border:10%}. text' => '

text

', + 'p{border:10em}. text' => '

text

', + 'p{border:1.5em}. text' => '

text

', + 'p{border-left:1px}. text' => '

text

', + 'p{border-right:1px}. text' => '

text

', + 'p{border-top:1px}. text' => '

text

', + 'p{border-bottom:1px}. text' => '

text

', + }, false) + + # multiple styles + assert_html_output({ + 'p{color:red; border-top:1px}. text' => '

text

', + 'p{color:red ; border-top:1px}. text' => '

text

', + 'p{color:red;border-top:1px}. text' => '

text

', + }, false) + + # styles with multiple values + assert_html_output({ + 'p{border:1px solid red;}. text' => '

text

', + 'p{border-top-left-radius: 10px 5px;}. text' => '

text

', + }, false) + end + + def test_invalid_styles_should_be_filtered + assert_html_output({ + 'p{invalid}. text' => '

text

', + 'p{invalid:red}. text' => '

text

', + 'p{color:(red)}. text' => '

text

', + 'p{color:red;invalid:blue}. text' => '

text

', + 'p{invalid:blue;color:red}. text' => '

text

', + 'p{color:"}. text' => '

p{color:"}. text

', + }, false) + end + + def test_inline_code + assert_html_output( + 'this is @some code@' => 'this is some code', + '@@' => '<Location /redmine>' + ) + end + + def test_nested_lists + raw = <<-RAW +# Item 1 +# Item 2 +** Item 2a +** Item 2b +# Item 3 +** Item 3a +RAW + + expected = <<-EXPECTED +
    +
  1. Item 1
  2. +
  3. Item 2 +
      +
    • Item 2a
    • +
    • Item 2b
    • +
    +
  4. +
  5. Item 3 +
      +
    • Item 3a
    • +
    +
  6. +
+EXPECTED + + assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') + end + + def test_escaping + assert_html_output( + 'this is a " => "

<script>some script;</script>

", + # do not escape pre/code tags + "
\nline 1\nline2
" => "
\nline 1\nline2
", + "
\nline 1\nline2
" => "
\nline 1\nline2
", + "
content
" => "
<div>content</div>
", + "HTML comment: " => "

HTML comment: <!-- no comments -->

", + " +<%= yield :header_tags -%> + + +
+
+
+
+
+ <%= 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1a/1ae20ea87464b5769eed93f455558d2b0e19460e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ae20ea87464b5769eed93f455558d2b0e19460e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1a/1ae3258fdcdf6aa143d4aa4fdfd79484152c6554.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ae3258fdcdf6aa143d4aa4fdfd79484152c6554.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,35 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module MembersHelper + def render_principals_for_new_members(project) + scope = Principal.active.sorted.not_member_of(project).like(params[:q]) + principal_count = scope.count + principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page'] + principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).all + + s = content_tag('div', principals_check_box_tags('membership[user_ids][]', principals), :id => 'principals') + + links = pagination_links_full(principal_pages, principal_count, :per_page_links => false) {|text, parameters, options| + link_to text, autocomplete_project_memberships_path(project, parameters.merge(:q => params[:q], :format => 'js')), :remote => true + } + + s + content_tag('p', links, :class => 'pagination') + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1a/1ae8ac309393cfb207f1a6763cafea128ad11893.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ae8ac309393cfb207f1a6763cafea128ad11893.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1a/1afc6535efb2815b924fdefa8ab10dacd7ba3c18.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1afc6535efb2815b924fdefa8ab10dacd7ba3c18.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1b/1b2837ef400afd5bc210e52e7203d86ba25be2b4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b2837ef400afd5bc210e52e7203d86ba25be2b4.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1b/1b56f9a09e214e641bcc7750244e8fb8e49ba7c3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b56f9a09e214e641bcc7750244e8fb8e49ba7c3.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,217 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class BoardsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules + + def setup + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:boards) + assert_not_nil assigns(:project) + end + + def test_index_not_found + get :index, :project_id => 97 + assert_response 404 + end + + def test_index_should_show_messages_if_only_one_board + Project.find(1).boards.slice(1..-1).each(&:destroy) + + get :index, :project_id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:topics) + end + + def test_show + get :show, :project_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topics) + end + + def test_show_should_display_sticky_messages_first + Message.update_all(:sticky => 0) + Message.update_all({:sticky => 1}, {:id => 1}) + + get :show, :project_id => 1, :id => 1 + assert_response :success + + topics = assigns(:topics) + assert_not_nil topics + assert topics.size > 1, "topics size was #{topics.size}" + assert topics.first.sticky? + assert topics.first.updated_on < topics.second.updated_on + end + + def test_show_should_display_message_with_last_reply_first + Message.update_all(:sticky => 0) + + # Reply to an old topic + old_topic = Message.where(:board_id => 1, :parent_id => nil).order('created_on ASC').first + reply = Message.new(:board_id => 1, :subject => 'New reply', :content => 'New reply', :author_id => 2) + old_topic.children << reply + + get :show, :project_id => 1, :id => 1 + assert_response :success + topics = assigns(:topics) + assert_not_nil topics + assert_equal old_topic, topics.first + end + + def test_show_with_permission_should_display_the_new_message_form + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 1 + assert_response :success + assert_template 'show' + + assert_select 'form#message-form' do + assert_select 'input[name=?]', 'message[subject]' + end + end + + def test_show_atom + get :show, :project_id => 1, :id => 1, :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:messages) + end + + def test_show_not_found + get :index, :project_id => 1, :id => 97 + assert_response 404 + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'board[parent_id]' do + assert_select 'option', (Project.find(1).boards.size + 1) + assert_select 'option[value=]', :text => ' ' + assert_select 'option[value=1]', :text => 'Help' + end + end + + def test_new_without_project_boards + Project.find(1).boards.delete_all + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'board[parent_id]', 0 + end + + def test_create + @request.session[:user_id] = 2 + assert_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.first(:order => 'id DESC') + assert_equal 'Testing', board.name + assert_equal 'Testing board creation', board.description + end + + def test_create_with_parent + @request.session[:user_id] = 2 + assert_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing', :parent_id => 2} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.first(:order => 'id DESC') + assert_equal Board.find(2), board.parent + end + + def test_create_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => '', :description => 'Testing board creation'} + end + assert_response :success + assert_template 'new' + end + + def test_edit + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_edit_with_parent + board = Board.generate!(:project_id => 1, :parent_id => 2) + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => board.id + assert_response :success + assert_template 'edit' + + assert_select 'select[name=?]', 'board[parent_id]' do + assert_select 'option[value=2][selected=selected]' + end + end + + def test_update + @request.session[:user_id] = 2 + assert_no_difference 'Board.count' do + put :update, :project_id => 1, :id => 2, :board => { :name => 'Testing', :description => 'Testing board update'} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + assert_equal 'Testing', Board.find(2).name + end + + def test_update_position + @request.session[:user_id] = 2 + put :update, :project_id => 1, :id => 2, :board => { :move_to => 'highest'} + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.find(2) + assert_equal 1, board.position + end + + def test_update_with_failure + @request.session[:user_id] = 2 + put :update, :project_id => 1, :id => 2, :board => { :name => '', :description => 'Testing board update'} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Board.count', -1 do + delete :destroy, :project_id => 1, :id => 2 + end + assert_redirected_to '/projects/ecookbook/settings/boards' + assert_nil Board.find_by_id(2) + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1b/1b80f432da3226981aed26b78872b070dbd0f9ee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b80f432da3226981aed26b78872b070dbd0f9ee.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,283 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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 a.disk_directory + assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory + + assert File.exist?(a.diskfile) + assert_equal 59, File.size(a.diskfile) + end + + def test_copy_should_preserve_attributes + a = Attachment.find(1) + copy = a.copy + + assert_save copy + copy = Attachment.order('id DESC').first + %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute| + assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different" + end + 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_equal [], 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 + + def test_move_from_root_to_target_directory_should_move_root_files + a = Attachment.find(20) + assert a.disk_directory.blank? + # Create a real file for this fixture + File.open(a.diskfile, "w") do |f| + f.write "test file at the root of files directory" + end + assert a.readable? + Attachment.move_from_root_to_target_directory + + a.reload + assert_equal '2012/05', a.disk_directory + assert a.readable? + end + + test "Attachmnet.attach_files 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 + + test "Attachmnet.attach_files 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 + + 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1b/1bcf725235890aaeb11816218989eb6ac291e46f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1bcf725235890aaeb11816218989eb6ac291e46f.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,81 @@ +<%= 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1b/1bd851612612b2edf1175e12bbce9b893a16c216.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1bd851612612b2edf1175e12bbce9b893a16c216.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,52 @@ +<%= error_messages_for 'tracker' %> + +
+
+ +

<%= 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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1c/1c27182fa8ab48906fb7a4d556796242f651644e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1c27182fa8ab48906fb7a4d556796242f651644e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,244 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Pagination + class Paginator + attr_reader :item_count, :per_page, :page, :page_param + + def initialize(*args) + if args.first.is_a?(ActionController::Base) + args.shift + ActiveSupport::Deprecation.warn "Paginator no longer takes a controller instance as the first argument. Remove it from #new arguments." + end + item_count, per_page, page, page_param = *args + + @item_count = item_count + @per_page = per_page + page = (page || 1).to_i + if page < 1 + page = 1 + end + @page = page + @page_param = page_param || :page + end + + def offset + (page - 1) * per_page + end + + def first_page + if item_count > 0 + 1 + end + end + + def previous_page + if page > 1 + page - 1 + end + end + + def next_page + if last_item < item_count + page + 1 + end + end + + def last_page + if item_count > 0 + (item_count - 1) / per_page + 1 + end + end + + def first_item + item_count == 0 ? 0 : (offset + 1) + end + + def last_item + l = first_item + per_page - 1 + l > item_count ? item_count : l + end + + def linked_pages + pages = [] + if item_count > 0 + pages += [first_page, page, last_page] + pages += ((page-2)..(page+2)).to_a.select {|p| p > first_page && p < last_page} + end + pages = pages.compact.uniq.sort + if pages.size > 1 + pages + else + [] + end + end + + def items_per_page + ActiveSupport::Deprecation.warn "Paginator#items_per_page will be removed. Use #per_page instead." + per_page + end + + def current + ActiveSupport::Deprecation.warn "Paginator#current will be removed. Use .offset instead of .current.offset." + self + end + end + + # Paginates the given scope or model. Returns a Paginator instance and + # the collection of objects for the current page. + # + # Options: + # :parameter name of the page parameter + # + # Examples: + # @user_pages, @users = paginate User.where(:status => 1) + # + def paginate(scope, options={}) + options = options.dup + finder_options = options.extract!( + :conditions, + :order, + :joins, + :include, + :select + ) + if scope.is_a?(Symbol) || finder_options.values.compact.any? + return deprecated_paginate(scope, finder_options, options) + end + + paginator = paginator(scope.count, options) + collection = scope.limit(paginator.per_page).offset(paginator.offset).to_a + + return paginator, collection + end + + def deprecated_paginate(arg, finder_options, options={}) + ActiveSupport::Deprecation.warn "#paginate with a Symbol and/or find options is depreceted and will be removed. Use a scope instead." + klass = arg.is_a?(Symbol) ? arg.to_s.classify.constantize : arg + scope = klass.scoped(finder_options) + paginate(scope, options) + end + + def paginator(item_count, options={}) + options.assert_valid_keys :parameter, :per_page + + page_param = options[:parameter] || :page + page = (params[page_param] || 1).to_i + per_page = options[:per_page] || per_page_option + Paginator.new(item_count, per_page, page, page_param) + end + + module Helper + include Redmine::I18n + + # Renders the pagination links for the given paginator. + # + # Options: + # :per_page_links if set to false, the "Per page" links are not rendered + # + def pagination_links_full(*args) + pagination_links_each(*args) do |text, parameters, options| + if block_given? + yield text, parameters, options + else + link_to text, params.merge(parameters), options + end + end + end + + # Yields the given block with the text and parameters + # for each pagination link and returns a string that represents the links + def pagination_links_each(paginator, count=nil, options={}, &block) + options.assert_valid_keys :per_page_links + + per_page_links = options.delete(:per_page_links) + per_page_links = false if count.nil? + page_param = paginator.page_param + + html = '' + if paginator.previous_page + # \xc2\xab(utf-8) = « + text = "\xc2\xab " + l(:label_previous) + html << yield(text, {page_param => paginator.previous_page}, :class => 'previous') + ' ' + end + + previous = nil + paginator.linked_pages.each do |page| + if previous && previous != page - 1 + html << content_tag('span', '...', :class => 'spacer') + ' ' + end + if page == paginator.page + html << content_tag('span', page.to_s, :class => 'current page') + else + html << yield(page.to_s, {page_param => page}, :class => 'page') + end + html << ' ' + previous = page + end + + if paginator.next_page + # \xc2\xbb(utf-8) = » + text = l(:label_next) + " \xc2\xbb" + html << yield(text, {page_param => paginator.next_page}, :class => 'next') + ' ' + end + + html << content_tag('span', "(#{paginator.first_item}-#{paginator.last_item}/#{paginator.item_count})", :class => 'items') + ' ' + + if per_page_links != false && links = per_page_links(paginator, &block) + html << content_tag('span', links.to_s, :class => 'per-page') + end + + html.html_safe + end + + # Renders the "Per page" links. + def per_page_links(paginator, &block) + values = per_page_options(paginator.per_page, paginator.item_count) + if values.any? + links = values.collect do |n| + if n == paginator.per_page + content_tag('span', n.to_s) + else + yield(n, :per_page => n, paginator.page_param => nil) + end + end + l(:label_display_per_page, links.join(', ')).html_safe + end + end + + def per_page_options(selected=nil, item_count=nil) + options = Setting.per_page_options_array + if item_count && options.any? + if item_count > options.first + max = options.detect {|value| value >= item_count} || item_count + else + max = item_count + end + options = options.select {|value| value <= max || value == selected} + end + if options.empty? || (options.size == 1 && options.first == selected) + [] + else + options + end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1d/1d1a40d68d66d098f38161f4b90f0e887e1b73d9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d1a40d68d66d098f38161f4b90f0e887e1b73d9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1d/1d70cf0be89de5fcd0a7d79fde026912fb2a0dcb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d70cf0be89de5fcd0a7d79fde026912fb2a0dcb.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1d/1d8c43c17d3233bffb7ec0f72aeb2fc2a1c616a9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d8c43c17d3233bffb7ec0f72aeb2fc2a1c616a9.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomValueTest < ActiveSupport::TestCase + fixtures :custom_fields, :custom_values, :users + + def test_default_value + field = CustomField.find_by_default_value('Default string') + assert_not_nil field + + v = CustomValue.new(:custom_field => field) + assert_equal 'Default string', v.value + + v = CustomValue.new(:custom_field => field, :value => 'Not empty') + assert_equal 'Not empty', v.value + end + + def test_sti_polymorphic_association + # Rails uses top level sti class for polymorphic association. See #3978. + assert !User.find(4).custom_values.empty? + assert !CustomValue.find(2).customized.nil? + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1d/1da882d06ce8605d4176f84b3f42ce44acc55377.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1da882d06ce8605d4176f84b3f42ce44acc55377.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1e/1e23a74057f558ab69d6074a0eabbe9696b5a8db.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1e23a74057f558ab69d6074a0eabbe9696b5a8db.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,135 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Acts + module Searchable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + # Options: + # * :columns - a column or an array of columns to search + # * :project_key - project foreign key (default to project_id) + # * :date_column - name of the datetime column (default to created_on) + # * :sort_order - name of the column used to sort results (default to :date_column or created_on) + # * :permission - permission required to search the model (default to :view_"objects") + def acts_as_searchable(options = {}) + return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods) + + cattr_accessor :searchable_options + self.searchable_options = options + + if searchable_options[:columns].nil? + raise 'No searchable column defined.' + elsif !searchable_options[:columns].is_a?(Array) + searchable_options[:columns] = [] << searchable_options[:columns] + end + + searchable_options[:project_key] ||= "#{table_name}.project_id" + searchable_options[:date_column] ||= "#{table_name}.created_on" + searchable_options[:order_column] ||= searchable_options[:date_column] + + # Should we search custom fields on this model ? + searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? + + send :include, Redmine::Acts::Searchable::InstanceMethods + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + # Searches the model for the given tokens + # projects argument can be either nil (will search all projects), a project or an array of projects + # Returns the results and the results count + def search(tokens, projects=nil, options={}) + if projects.is_a?(Array) && projects.empty? + # no results + return [[], 0] + end + + # TODO: make user an argument + user = User.current + tokens = [] << tokens unless tokens.is_a?(Array) + projects = [] << projects unless projects.nil? || projects.is_a?(Array) + + limit_options = {} + limit_options[:limit] = options[:limit] if options[:limit] + + columns = searchable_options[:columns] + columns = columns[0..0] if options[:titles_only] + + token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"} + + if !options[:titles_only] && searchable_options[:search_custom_fields] + searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true) + searchable_custom_fields.each do |field| + sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" + + " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" + + " AND #{CustomValue.table_name}.custom_field_id = #{field.id})" + + " AND #{field.visibility_by_project_condition(searchable_options[:project_key], user)}" + token_clauses << sql + end + end + + sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + + tokens_conditions = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort] + + scope = self.scoped + project_conditions = [] + if searchable_options.has_key?(:permission) + project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project) + elsif respond_to?(:visible) + scope = scope.visible(user) + else + ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option." + project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym) + end + # TODO: use visible scope options instead + project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? + project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ') + + results = [] + results_count = 0 + + scope = scope. + includes(searchable_options[:include]). + order("#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')). + where(project_conditions). + where(tokens_conditions) + + results_count = scope.count + + scope_with_limit = scope.limit(options[:limit]) + if options[:offset] + scope_with_limit = scope_with_limit.where("#{searchable_options[:date_column]} #{options[:before] ? '<' : '>'} ?", options[:offset]) + end + results = scope_with_limit.all + + [results, results_count] + end + end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1e/1e54d19d4970142ec949fe4202f5ffe30b1f8dbf.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1e54d19d4970142ec949fe4202f5ffe30b1f8dbf.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,78 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::DisabledRestApiTest < 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 = '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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1f/1f0cbd778ac799db40d158f816d90f939c73e314.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f0cbd778ac799db40d158f816d90f939c73e314.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +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 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1f/1f46b0be9dff718da9b5754753e6d89331b5e22e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f46b0be9dff718da9b5754753e6d89331b5e22e.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,24 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module AuthSourcesHelper + def auth_source_partial_name(auth_source) + "form_#{auth_source.class.name.underscore}" + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 .svn/pristine/1f/1f882abb5f1807e56ffd3445088d6c8aa33ef8e5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f882abb5f1807e56ffd3445088d6c8aa33ef8e5.svn-base Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,593 @@ +/* Redmine - project management software + Copyright (C) 2006-2013 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 = $('
"+(o[0]>0&&I==o[1]-1?'
':""):""),F+=U}B+=F}return B+=x+($.ui.ie6&&!e.inline?'':""),e._keyEvent=!1,B},_generateMonthYearHeader:function(e,t,n,r,i,s,o,u){var a=this._get(e,"changeMonth"),f=this._get(e,"changeYear"),l=this._get(e,"showMonthAfterYear"),c='
',h="";if(s||!a)h+=''+o[t]+"";else{var p=r&&r.getFullYear()==n,d=i&&i.getFullYear()==n;h+='"}l||(c+=h+(s||!a||!f?" ":""));if(!e.yearshtml){e.yearshtml="";if(s||!f)c+=''+n+"";else{var m=this._get(e,"yearRange").split(":"),g=(new Date).getFullYear(),y=function(e){var t=e.match(/c[+-].*/)?n+parseInt(e.substring(1),10):e.match(/[+-].*/)?g+parseInt(e,10):parseInt(e,10);return isNaN(t)?g:t},b=y(m[0]),w=Math.max(b,y(m[1]||""));b=r?Math.max(b,r.getFullYear()):b,w=i?Math.min(w,i.getFullYear()):w,e.yearshtml+='",c+=e.yearshtml,e.yearshtml=null}}return c+=this._get(e,"yearSuffix"),l&&(c+=(s||!a||!f?" ":"")+h),c+="
",c},_adjustInstDate:function(e,t,n){var r=e.drawYear+(n=="Y"?t:0),i=e.drawMonth+(n=="M"?t:0),s=Math.min(e.selectedDay,this._getDaysInMonth(r,i))+(n=="D"?t:0),o=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(r,i,s)));e.selectedDay=o.getDate(),e.drawMonth=e.selectedMonth=o.getMonth(),e.drawYear=e.selectedYear=o.getFullYear(),(n=="M"||n=="Y")&&this._notifyChange(e)},_restrictMinMax:function(e,t){var n=this._getMinMaxDate(e,"min"),r=this._getMinMaxDate(e,"max"),i=n&&tr?r:i,i},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return t==null?[1,1]:typeof t=="number"?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return(new Date(e,t,1)).getDay()},_canAdjustMonth:function(e,t,n,r){var i=this._getNumberOfMonths(e),s=this._daylightSavingAdjust(new Date(n,r+(t<0?t:i[0]*i[1]),1));return t<0&&s.setDate(this._getDaysInMonth(s.getFullYear(),s.getMonth())),this._isInRange(e,s)},_isInRange:function(e,t){var n=this._getMinMaxDate(e,"min"),r=this._getMinMaxDate(e,"max");return(!n||t.getTime()>=n.getTime())&&(!r||t.getTime()<=r.getTime())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t=typeof t!="string"?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,n,r){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var i=t?typeof t=="object"?t:this._daylightSavingAdjust(new Date(r,n,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),i,this._getFormatConfig(e))}}),$.fn.datepicker=function(e){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find(document.body).append($.datepicker.dpDiv),$.datepicker.initialized=!0);var t=Array.prototype.slice.call(arguments,1);return typeof e!="string"||e!="isDisabled"&&e!="getDate"&&e!="widget"?e=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t)):this.each(function(){typeof e=="string"?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this].concat(t)):$.datepicker._attachDatepicker(this,e)}):$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.9.2",window["DP_jQuery_"+dpuuid]=$})(jQuery);(function(e,t){var n="ui-dialog ui-widget ui-widget-content ui-corner-all ",r={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.9.2",options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var n=e(this).css(t).offset().top;n<0&&e(this).css("top",t.top-n)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.oldPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.options.title=this.options.title||this.originalTitle;var t=this,r=this.options,i=r.title||" ",s,o,u,a,f;s=(this.uiDialog=e("
")).addClass(n+r.dialogClass).css({display:"none",outline:0,zIndex:r.zIndex}).attr("tabIndex",-1).keydown(function(n){r.closeOnEscape&&!n.isDefaultPrevented()&&n.keyCode&&n.keyCode===e.ui.keyCode.ESCAPE&&(t.close(n),n.preventDefault())}).mousedown(function(e){t.moveToTop(!1,e)}).appendTo("body"),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(s),o=(this.uiDialogTitlebar=e("
")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").bind("mousedown",function(){s.focus()}).prependTo(s),u=e("").addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").click(function(e){e.preventDefault(),t.close(e)}).appendTo(o),(this.uiDialogTitlebarCloseText=e("")).addClass("ui-icon ui-icon-closethick").text(r.closeText).appendTo(u),a=e("").uniqueId().addClass("ui-dialog-title").html(i).prependTo(o),f=(this.uiDialogButtonPane=e("
")).addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),(this.uiButtonSet=e("
")).addClass("ui-dialog-buttonset").appendTo(f),s.attr({role:"dialog","aria-labelledby":a.attr("id")}),o.find("*").add(o).disableSelection(),this._hoverable(u),this._focusable(u),r.draggable&&e.fn.draggable&&this._makeDraggable(),r.resizable&&e.fn.resizable&&this._makeResizable(),this._createButtons(r.buttons),this._isOpen=!1,e.fn.bgiframe&&s.bgiframe(),this._on(s,{keydown:function(t){if(!r.modal||t.keyCode!==e.ui.keyCode.TAB)return;var n=e(":tabbable",s),i=n.filter(":first"),o=n.filter(":last");if(t.target===o[0]&&!t.shiftKey)return i.focus(1),!1;if(t.target===i[0]&&t.shiftKey)return o.focus(1),!1}})},_init:function(){this.options.autoOpen&&this.open()},_destroy:function(){var e,t=this.oldPosition;this.overlay&&this.overlay.destroy(),this.uiDialog.hide(),this.element.removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),this.uiDialog.remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.children().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},close:function(t){var n=this,r,i;if(!this._isOpen)return;if(!1===this._trigger("beforeClose",t))return;return this._isOpen=!1,this.overlay&&this.overlay.destroy(),this.options.hide?this._hide(this.uiDialog,this.options.hide,function(){n._trigger("close",t)}):(this.uiDialog.hide(),this._trigger("close",t)),e.ui.dialog.overlay.resize(),this.options.modal&&(r=0,e(".ui-dialog").each(function(){this!==n.uiDialog[0]&&(i=e(this).css("z-index"),isNaN(i)||(r=Math.max(r,i)))}),e.ui.dialog.maxZ=r),this},isOpen:function(){return this._isOpen},moveToTop:function(t,n){var r=this.options,i;return r.modal&&!t||!r.stack&&!r.modal?this._trigger("focus",n):(r.zIndex>e.ui.dialog.maxZ&&(e.ui.dialog.maxZ=r.zIndex),this.overlay&&(e.ui.dialog.maxZ+=1,e.ui.dialog.overlay.maxZ=e.ui.dialog.maxZ,this.overlay.$el.css("z-index",e.ui.dialog.overlay.maxZ)),i={scrollTop:this.element.scrollTop(),scrollLeft:this.element.scrollLeft()},e.ui.dialog.maxZ+=1,this.uiDialog.css("z-index",e.ui.dialog.maxZ),this.element.attr(i),this._trigger("focus",n),this)},open:function(){if(this._isOpen)return;var t,n=this.options,r=this.uiDialog;return this._size(),this._position(n.position),r.show(n.show),this.overlay=n.modal?new e.ui.dialog.overlay(this):null,this.moveToTop(!0),t=this.element.find(":tabbable"),t.length||(t=this.uiDialogButtonPane.find(":tabbable"),t.length||(t=r)),t.eq(0).focus(),this._isOpen=!0,this._trigger("open"),this},_createButtons:function(t){var n=this,r=!1;this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),typeof t=="object"&&t!==null&&e.each(t,function(){return!(r=!0)}),r?(e.each(t,function(t,r){var i,s;r=e.isFunction(r)?{click:r,text:t}:r,r=e.extend({type:"button"},r),s=r.click,r.click=function(){s.apply(n.element[0],arguments)},i=e("",r).appendTo(n.uiButtonSet),e.fn.button&&i.button()}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog)):this.uiDialog.removeClass("ui-dialog-buttons")},_makeDraggable:function(){function r(e){return{position:e.position,offset:e.offset}}var t=this,n=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(n,i){e(this).addClass("ui-dialog-dragging"),t._trigger("dragStart",n,r(i))},drag:function(e,n){t._trigger("drag",e,r(n))},stop:function(i,s){n.position=[s.position.left-t.document.scrollLeft(),s.position.top-t.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),t._trigger("dragStop",i,r(s)),e.ui.dialog.overlay.resize()}})},_makeResizable:function(n){function u(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}n=n===t?this.options.resizable:n;var r=this,i=this.options,s=this.uiDialog.css("position"),o=typeof n=="string"?n:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:i.maxWidth,maxHeight:i.maxHeight,minWidth:i.minWidth,minHeight:this._minHeight(),handles:o,start:function(t,n){e(this).addClass("ui-dialog-resizing"),r._trigger("resizeStart",t,u(n))},resize:function(e,t){r._trigger("resize",e,u(t))},stop:function(t,n){e(this).removeClass("ui-dialog-resizing"),i.height=e(this).height(),i.width=e(this).width(),r._trigger("resizeStop",t,u(n)),e.ui.dialog.overlay.resize()}}).css("position",s).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var e=this.options;return e.height==="auto"?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(t){var n=[],r=[0,0],i;if(t){if(typeof t=="string"||typeof t=="object"&&"0"in t)n=t.split?t.split(" "):[t[0],t[1]],n.length===1&&(n[1]=n[0]),e.each(["left","top"],function(e,t){+n[e]===n[e]&&(r[e]=n[e],n[e]=t)}),t={my:n[0]+(r[0]<0?r[0]:"+"+r[0])+" "+n[1]+(r[1]<0?r[1]:"+"+r[1]),at:n.join(" ")};t=e.extend({},e.ui.dialog.prototype.options.position,t)}else t=e.ui.dialog.prototype.options.position;i=this.uiDialog.is(":visible"),i||this.uiDialog.show(),this.uiDialog.position(t),i||this.uiDialog.hide()},_setOptions:function(t){var n=this,s={},o=!1;e.each(t,function(e,t){n._setOption(e,t),e in r&&(o=!0),e in i&&(s[e]=t)}),o&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",s)},_setOption:function(t,r){var i,s,o=this.uiDialog;switch(t){case"buttons":this._createButtons(r);break;case"closeText":this.uiDialogTitlebarCloseText.text(""+r);break;case"dialogClass":o.removeClass(this.options.dialogClass).addClass(n+r);break;case"disabled":r?o.addClass("ui-dialog-disabled"):o.removeClass("ui-dialog-disabled");break;case"draggable":i=o.is(":data(draggable)"),i&&!r&&o.draggable("destroy"),!i&&r&&this._makeDraggable();break;case"position":this._position(r);break;case"resizable":s=o.is(":data(resizable)"),s&&!r&&o.resizable("destroy"),s&&typeof r=="string"&&o.resizable("option","handles",r),!s&&r!==!1&&this._makeResizable(r);break;case"title":e(".ui-dialog-title",this.uiDialogTitlebar).html(""+(r||" "))}this._super(t,r)},_size:function(){var t,n,r,i=this.options,s=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),i.minWidth>i.width&&(i.width=i.minWidth),t=this.uiDialog.css({height:"auto",width:i.width}).outerHeight(),n=Math.max(0,i.minHeight-t),i.height==="auto"?e.support.minHeight?this.element.css({minHeight:n,height:"auto"}):(this.uiDialog.show(),r=this.element.css("height","auto").height(),s||this.uiDialog.hide(),this.element.height(Math.max(r,n))):this.element.height(Math.max(i.height-t,0)),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),e.extend(e.ui.dialog,{uuid:0,maxZ:0,getTitleId:function(e){var t=e.attr("id");return t||(this.uuid+=1,t=this.uuid),"ui-dialog-title-"+t},overlay:function(t){this.$el=e.ui.dialog.overlay.create(t)}}),e.extend(e.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:e.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(e){return e+".dialog-overlay"}).join(" "),create:function(t){this.instances.length===0&&(setTimeout(function(){e.ui.dialog.overlay.instances.length&&e(document).bind(e.ui.dialog.overlay.events,function(t){if(e(t.target).zIndex()").addClass("ui-widget-overlay");return e(document).bind("keydown.dialog-overlay",function(r){var i=e.ui.dialog.overlay.instances;i.length!==0&&i[i.length-1]===n&&t.options.closeOnEscape&&!r.isDefaultPrevented()&&r.keyCode&&r.keyCode===e.ui.keyCode.ESCAPE&&(t.close(r),r.preventDefault())}),n.appendTo(document.body).css({width:this.width(),height:this.height()}),e.fn.bgiframe&&n.bgiframe(),this.instances.push(n),n},destroy:function(t){var n=e.inArray(t,this.instances),r=0;n!==-1&&this.oldInstances.push(this.instances.splice(n,1)[0]),this.instances.length===0&&e([document,window]).unbind(".dialog-overlay"),t.height(0).width(0).remove(),e.each(this.instances,function(){r=Math.max(r,this.css("z-index"))}),this.maxZ=r},height:function(){var t,n;return e.ui.ie?(t=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),n=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),t",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,e.proxy(function(e){this.options.disabled&&e.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(e){e.preventDefault()},"click .ui-state-disabled > a":function(e){e.preventDefault()},"click .ui-menu-item:has(a)":function(t){var r=e(t.target).closest(".ui-menu-item");!n&&r.not(".ui-state-disabled").length&&(n=!0,this.select(t),r.has(".ui-menu").length?this.expand(t):this.element.is(":focus")||(this.element.trigger("focus",[!0]),this.active&&this.active.parents(".ui-menu").length===1&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(t){var n=e(t.currentTarget);n.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(t,n)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(e,t){var n=this.active||this.element.children(".ui-menu-item").eq(0);t||this.focus(e,n)},blur:function(t){this._delay(function(){e.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){e(t.target).closest(".ui-menu").length||this.collapseAll(t),n=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").andSelf().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var t=e(this);t.data("ui-menu-submenu-carat")&&t.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(t){function a(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var n,r,i,s,o,u=!0;switch(t.keyCode){case e.ui.keyCode.PAGE_UP:this.previousPage(t);break;case e.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case e.ui.keyCode.HOME:this._move("first","first",t);break;case e.ui.keyCode.END:this._move("last","last",t);break;case e.ui.keyCode.UP:this.previous(t);break;case e.ui.keyCode.DOWN:this.next(t);break;case e.ui.keyCode.LEFT:this.collapse(t);break;case e.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case e.ui.keyCode.ENTER:case e.ui.keyCode.SPACE:this._activate(t);break;case e.ui.keyCode.ESCAPE:this.collapse(t);break;default:u=!1,r=this.previousFilter||"",i=String.fromCharCode(t.keyCode),s=!1,clearTimeout(this.filterTimer),i===r?s=!0:i=r+i,o=new RegExp("^"+a(i),"i"),n=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())}),n=s&&n.index(this.active.next())!==-1?this.active.nextAll(".ui-menu-item"):n,n.length||(i=String.fromCharCode(t.keyCode),o=new RegExp("^"+a(i),"i"),n=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())})),n.length?(this.focus(t,n),n.length>1?(this.previousFilter=i,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}u&&t.preventDefault()},_activate:function(e){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(e):this.select(e))},refresh:function(){var t,n=this.options.icons.submenu,r=this.element.find(this.options.menus);r.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=e(this),r=t.prev("a"),i=e("").addClass("ui-menu-icon ui-icon "+n).data("ui-menu-submenu-carat",!0);r.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",r.attr("id"))}),t=r.add(this.element),t.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),t.children(":not(.ui-menu-item)").each(function(){var t=e(this);/[^\-â€â€Ã¢â‚¬â€œ\s]/.test(t.text())||t.addClass("ui-widget-content ui-menu-divider")}),t.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!e.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},focus:function(e,t){var n,r;this.blur(e,e&&e.type==="focus"),this._scrollIntoView(t),this.active=t.first(),r=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",r.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),e&&e.type==="keydown"?this._close():this.timer=this._delay(function(){this._close()},this.delay),n=t.children(".ui-menu"),n.length&&/^mouse/.test(e.type)&&this._startOpening(n),this.activeMenu=t.parent(),this._trigger("focus",e,{item:t})},_scrollIntoView:function(t){var n,r,i,s,o,u;this._hasScroll()&&(n=parseFloat(e.css(this.activeMenu[0],"borderTopWidth"))||0,r=parseFloat(e.css(this.activeMenu[0],"paddingTop"))||0,i=t.offset().top-this.activeMenu.offset().top-n-r,s=this.activeMenu.scrollTop(),o=this.activeMenu.height(),u=t.height(),i<0?this.activeMenu.scrollTop(s+i):i+u>o&&this.activeMenu.scrollTop(s+i-o+u))},blur:function(e,t){t||clearTimeout(this.timer);if(!this.active)return;this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",e,{item:this.active})},_startOpening:function(e){clearTimeout(this.timer);if(e.attr("aria-hidden")!=="true")return;this.timer=this._delay(function(){this._close(),this._open(e)},this.delay)},_open:function(t){var n=e.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(t.parents(".ui-menu")).hide().attr("aria-hidden","true"),t.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(n)},collapseAll:function(t,n){clearTimeout(this.timer),this.timer=this._delay(function(){var r=n?this.element:e(t&&t.target).closest(this.element.find(".ui-menu"));r.length||(r=this.element),this._close(r),this.blur(t),this.activeMenu=r},this.delay)},_close:function(e){e||(e=this.active?this.active.parent():this.element),e.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(e){var t=this.active&&this.active.parent().closest(".ui-menu-item",this.element);t&&t.length&&(this._close(),this.focus(e,t))},expand:function(e){var t=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();t&&t.length&&(this._open(t.parent()),this._delay(function(){this.focus(e,t)}))},next:function(e){this._move("next","first",e)},previous:function(e){this._move("prev","last",e)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(e,t,n){var r;this.active&&(e==="first"||e==="last"?r=this.active[e==="first"?"prevAll":"nextAll"](".ui-menu-item").eq(-1):r=this.active[e+"All"](".ui-menu-item").eq(0));if(!r||!r.length||!this.active)r=this.activeMenu.children(".ui-menu-item")[t]();this.focus(n,r)},nextPage:function(t){var n,r,i;if(!this.active){this.next(t);return}if(this.isLastItem())return;this._hasScroll()?(r=this.active.offset().top,i=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return n=e(this),n.offset().top-r-i<0}),this.focus(t,n)):this.focus(t,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())},previousPage:function(t){var n,r,i;if(!this.active){this.next(t);return}if(this.isFirstItem())return;this._hasScroll()?(r=this.active.offset().top,i=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return n=e(this),n.offset().top-r+i>0}),this.focus(t,n)):this.focus(t,this.activeMenu.children(".ui-menu-item").first())},_hasScroll:function(){return this.element.outerHeight()
").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(e){return e===t?this._value():(this._setOption("value",e),this)},_setOption:function(e,t){e==="value"&&(this.options.value=t,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),this._super(e,t)},_value:function(){var e=this.options.value;return typeof e!="number"&&(e=0),Math.min(this.options.max,Math.max(this.min,e))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var e=this.value(),t=this._percentage();this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),this.valueDiv.toggle(e>this.min).toggleClass("ui-corner-right",e===this.options.max).width(t.toFixed(0)+"%"),this.element.attr("aria-valuenow",e)}})})(jQuery);(function(e,t){var n=5;e.widget("ui.slider",e.ui.mouse,{version:"1.9.2",widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var t,r,i=this.options,s=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),o="",u=[];this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(i.disabled?" ui-slider-disabled ui-disabled":"")),this.range=e([]),i.range&&(i.range===!0&&(i.values||(i.values=[this._valueMin(),this._valueMin()]),i.values.length&&i.values.length!==2&&(i.values=[i.values[0],i.values[0]])),this.range=e("
").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(i.range==="min"||i.range==="max"?" ui-slider-range-"+i.range:""))),r=i.values&&i.values.length||1;for(t=s.length;tn&&(i=n,s=e(this),o=t)}),c.range===!0&&this.values(1)===c.min&&(o+=1,s=e(this.handles[o])),u=this._start(t,o),u===!1?!1:(this._mouseSliding=!0,this._handleIndex=o,s.addClass("ui-state-active").focus(),a=s.offset(),f=!e(t.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=f?{left:0,top:0}:{left:t.pageX-a.left-s.width()/2,top:t.pageY-a.top-s.height()/2-(parseInt(s.css("borderTopWidth"),10)||0)-(parseInt(s.css("borderBottomWidth"),10)||0)+(parseInt(s.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(t,o,r),this._animateOff=!0,!0))},_mouseStart:function(){return!0},_mouseDrag:function(e){var t={x:e.pageX,y:e.pageY},n=this._normValueFromMouse(t);return this._slide(e,this._handleIndex,n),!1},_mouseStop:function(e){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(e,this._handleIndex),this._change(e,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(e){var t,n,r,i,s;return this.orientation==="horizontal"?(t=this.elementSize.width,n=e.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(t=this.elementSize.height,n=e.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),r=n/t,r>1&&(r=1),r<0&&(r=0),this.orientation==="vertical"&&(r=1-r),i=this._valueMax()-this._valueMin(),s=this._valueMin()+r*i,this._trimAlignValue(s)},_start:function(e,t){var n={handle:this.handles[t],value:this.value()};return this.options.values&&this.options.values.length&&(n.value=this.values(t),n.values=this.values()),this._trigger("start",e,n)},_slide:function(e,t,n){var r,i,s;this.options.values&&this.options.values.length?(r=this.values(t?0:1),this.options.values.length===2&&this.options.range===!0&&(t===0&&n>r||t===1&&n1){this.options.values[t]=this._trimAlignValue(n),this._refreshValue(),this._change(null,t);return}if(!arguments.length)return this._values();if(!e.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(t):this.value();r=this.options.values,i=arguments[0];for(s=0;s=this._valueMax())return this._valueMax();var t=this.options.step>0?this.options.step:1,n=(e-this._valueMin())%t,r=e-n;return Math.abs(n)*2>=t&&(r+=n>0?t:-t),parseFloat(r.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var t,n,r,i,s,o=this.options.range,u=this.options,a=this,f=this._animateOff?!1:u.animate,l={};this.options.values&&this.options.values.length?this.handles.each(function(r){n=(a.values(r)-a._valueMin())/(a._valueMax()-a._valueMin())*100,l[a.orientation==="horizontal"?"left":"bottom"]=n+"%",e(this).stop(1,1)[f?"animate":"css"](l,u.animate),a.options.range===!0&&(a.orientation==="horizontal"?(r===0&&a.range.stop(1,1)[f?"animate":"css"]({left:n+"%"},u.animate),r===1&&a.range[f?"animate":"css"]({width:n-t+"%"},{queue:!1,duration:u.animate})):(r===0&&a.range.stop(1,1)[f?"animate":"css"]({bottom:n+"%"},u.animate),r===1&&a.range[f?"animate":"css"]({height:n-t+"%"},{queue:!1,duration:u.animate}))),t=n}):(r=this.value(),i=this._valueMin(),s=this._valueMax(),n=s!==i?(r-i)/(s-i)*100:0,l[this.orientation==="horizontal"?"left":"bottom"]=n+"%",this.handle.stop(1,1)[f?"animate":"css"](l,u.animate),o==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[f?"animate":"css"]({width:n+"%"},u.animate),o==="max"&&this.orientation==="horizontal"&&this.range[f?"animate":"css"]({width:100-n+"%"},{queue:!1,duration:u.animate}),o==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[f?"animate":"css"]({height:n+"%"},u.animate),o==="max"&&this.orientation==="vertical"&&this.range[f?"animate":"css"]({height:100-n+"%"},{queue:!1,duration:u.animate}))}})})(jQuery);(function(e){function t(e){return function(){var t=this.element.val();e.apply(this,arguments),this._refresh(),t!==this.element.val()&&this._trigger("change")}}e.widget("ui.spinner",{version:"1.9.2",defaultElement:"",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var t={},n=this.element;return e.each(["min","max","step"],function(e,r){var i=n.attr(r);i!==undefined&&i.length&&(t[r]=i)}),t},_events:{keydown:function(e){this._start(e)&&this._keydown(e)&&e.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(e){if(this.cancelBlur){delete this.cancelBlur;return}this._refresh(),this.previous!==this.element.val()&&this._trigger("change",e)},mousewheel:function(e,t){if(!t)return;if(!this.spinning&&!this._start(e))return!1;this._spin((t>0?1:-1)*this.options.step,e),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(e)},100),e.preventDefault()},"mousedown .ui-spinner-button":function(t){function r(){var e=this.element[0]===this.document[0].activeElement;e||(this.element.focus(),this.previous=n,this._delay(function(){this.previous=n}))}var n;n=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),t.preventDefault(),r.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,r.call(this)});if(this._start(t)===!1)return;this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(t){if(!e(t.currentTarget).hasClass("ui-state-active"))return;if(this._start(t)===!1)return!1;this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var e=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=e.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(e.height()*.5)&&e.height()>0&&e.height(e.height()),this.options.disabled&&this.disable()},_keydown:function(t){var n=this.options,r=e.ui.keyCode;switch(t.keyCode){case r.UP:return this._repeat(null,1,t),!0;case r.DOWN:return this._repeat(null,-1,t),!0;case r.PAGE_UP:return this._repeat(null,n.page,t),!0;case r.PAGE_DOWN:return this._repeat(null,-n.page,t),!0}return!1},_uiSpinnerHtml:function(){return""},_buttonHtml:function(){return""+""+""+""+""},_start:function(e){return!this.spinning&&this._trigger("start",e)===!1?!1:(this.counter||(this.counter=1),this.spinning=!0,!0)},_repeat:function(e,t,n){e=e||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,t,n)},e),this._spin(t*this.options.step,n)},_spin:function(e,t){var n=this.value()||0;this.counter||(this.counter=1),n=this._adjustValue(n+e*this._increment(this.counter));if(!this.spinning||this._trigger("spin",t,{value:n})!==!1)this._value(n),this.counter++},_increment:function(t){var n=this.options.incremental;return n?e.isFunction(n)?n(t):Math.floor(t*t*t/5e4-t*t/500+17*t/200+1):1},_precision:function(){var e=this._precisionOf(this.options.step);return this.options.min!==null&&(e=Math.max(e,this._precisionOf(this.options.min))),e},_precisionOf:function(e){var t=e.toString(),n=t.indexOf(".");return n===-1?0:t.length-n-1},_adjustValue:function(e){var t,n,r=this.options;return t=r.min!==null?r.min:0,n=e-t,n=Math.round(n/r.step)*r.step,e=t+n,e=parseFloat(e.toFixed(this._precision())),r.max!==null&&e>r.max?r.max:r.min!==null&&e1&&e.href.replace(r,"")===location.href.replace(r,"").replace(/\s/g,"%20")}var n=0,r=/#.*$/;e.widget("ui.tabs",{version:"1.9.2",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_create:function(){var t=this,n=this.options,r=n.active,i=location.hash.substring(1);this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",n.collapsible).delegate(".ui-tabs-nav > li","mousedown"+this.eventNamespace,function(t){e(this).is(".ui-state-disabled")&&t.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){e(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this._processTabs();if(r===null){i&&this.tabs.each(function(t,n){if(e(n).attr("aria-controls")===i)return r=t,!1}),r===null&&(r=this.tabs.index(this.tabs.filter(".ui-tabs-active")));if(r===null||r===-1)r=this.tabs.length?0:!1}r!==!1&&(r=this.tabs.index(this.tabs.eq(r)),r===-1&&(r=n.collapsible?!1:0)),n.active=r,!n.collapsible&&n.active===!1&&this.anchors.length&&(n.active=0),e.isArray(n.disabled)&&(n.disabled=e.unique(n.disabled.concat(e.map(this.tabs.filter(".ui-state-disabled"),function(e){return t.tabs.index(e)}))).sort()),this.options.active!==!1&&this.anchors.length?this.active=this._findActive(this.options.active):this.active=e(),this._refresh(),this.active.length&&this.load(n.active)},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):e()}},_tabKeydown:function(t){var n=e(this.document[0].activeElement).closest("li"),r=this.tabs.index(n),i=!0;if(this._handlePageNav(t))return;switch(t.keyCode){case e.ui.keyCode.RIGHT:case e.ui.keyCode.DOWN:r++;break;case e.ui.keyCode.UP:case e.ui.keyCode.LEFT:i=!1,r--;break;case e.ui.keyCode.END:r=this.anchors.length-1;break;case e.ui.keyCode.HOME:r=0;break;case e.ui.keyCode.SPACE:t.preventDefault(),clearTimeout(this.activating),this._activate(r);return;case e.ui.keyCode.ENTER:t.preventDefault(),clearTimeout(this.activating),this._activate(r===this.options.active?!1:r);return;default:return}t.preventDefault(),clearTimeout(this.activating),r=this._focusNextTab(r,i),t.ctrlKey||(n.attr("aria-selected","false"),this.tabs.eq(r).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",r)},this.delay))},_panelKeydown:function(t){if(this._handlePageNav(t))return;t.ctrlKey&&t.keyCode===e.ui.keyCode.UP&&(t.preventDefault(),this.active.focus())},_handlePageNav:function(t){if(t.altKey&&t.keyCode===e.ui.keyCode.PAGE_UP)return this._activate(this._focusNextTab(this.options.active-1,!1)),!0;if(t.altKey&&t.keyCode===e.ui.keyCode.PAGE_DOWN)return this._activate(this._focusNextTab(this.options.active+1,!0)),!0},_findNextTab:function(t,n){function i(){return t>r&&(t=0),t<0&&(t=r),t}var r=this.tabs.length-1;while(e.inArray(i(),this.options.disabled)!==-1)t=n?t+1:t-1;return t},_focusNextTab:function(e,t){return e=this._findNextTab(e,t),this.tabs.eq(e).focus(),e},_setOption:function(e,t){if(e==="active"){this._activate(t);return}if(e==="disabled"){this._setupDisabled(t);return}this._super(e,t),e==="collapsible"&&(this.element.toggleClass("ui-tabs-collapsible",t),!t&&this.options.active===!1&&this._activate(0)),e==="event"&&this._setupEvents(t),e==="heightStyle"&&this._setupHeightStyle(t)},_tabId:function(e){return e.attr("aria-controls")||"ui-tabs-"+i()},_sanitizeSelector:function(e){return e?e.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var t=this.options,n=this.tablist.children(":has(a[href])");t.disabled=e.map(n.filter(".ui-state-disabled"),function(e){return n.index(e)}),this._processTabs(),t.active===!1||!this.anchors.length?(t.active=!1,this.active=e()):this.active.length&&!e.contains(this.tablist[0],this.active[0])?this.tabs.length===t.disabled.length?(t.active=!1,this.active=e()):this._activate(this._findNextTab(Math.max(0,t.active-1),!1)):t.active=this.tabs.index(this.active),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-expanded":"false","aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-expanded":"true","aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var t=this;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist"),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return e("a",this)[0]}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=e(),this.anchors.each(function(n,r){var i,o,u,a=e(r).uniqueId().attr("id"),f=e(r).closest("li"),l=f.attr("aria-controls");s(r)?(i=r.hash,o=t.element.find(t._sanitizeSelector(i))):(u=t._tabId(f),i="#"+u,o=t.element.find(i),o.length||(o=t._createPanel(u),o.insertAfter(t.panels[n-1]||t.tablist)),o.attr("aria-live","polite")),o.length&&(t.panels=t.panels.add(o)),l&&f.data("ui-tabs-aria-controls",l),f.attr({"aria-controls":i.substring(1),"aria-labelledby":a}),o.attr("aria-labelledby",a)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel")},_getList:function(){return this.element.find("ol,ul").eq(0)},_createPanel:function(t){return e("
").attr("id",t).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(t){e.isArray(t)&&(t.length?t.length===this.anchors.length&&(t=!0):t=!1);for(var n=0,r;r=this.tabs[n];n++)t===!0||e.inArray(n,t)!==-1?e(r).addClass("ui-state-disabled").attr("aria-disabled","true"):e(r).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=t},_setupEvents:function(t){var n={click:function(e){e.preventDefault()}};t&&e.each(t.split(" "),function(e,t){n[t]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(this.anchors,n),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(t){var n,r,i=this.element.parent();t==="fill"?(e.support.minHeight||(r=i.css("overflow"),i.css("overflow","hidden")),n=i.height(),this.element.siblings(":visible").each(function(){var t=e(this),r=t.css("position");if(r==="absolute"||r==="fixed")return;n-=t.outerHeight(!0)}),r&&i.css("overflow",r),this.element.children().not(this.panels).each(function(){n-=e(this).outerHeight(!0)}),this.panels.each(function(){e(this).height(Math.max(0,n-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):t==="auto"&&(n=0,this.panels.each(function(){n=Math.max(n,e(this).height("").height())}).height(n))},_eventHandler:function(t){var n=this.options,r=this.active,i=e(t.currentTarget),s=i.closest("li"),o=s[0]===r[0],u=o&&n.collapsible,a=u?e():this._getPanelForTab(s),f=r.length?this._getPanelForTab(r):e(),l={oldTab:r,oldPanel:f,newTab:u?e():s,newPanel:a};t.preventDefault();if(s.hasClass("ui-state-disabled")||s.hasClass("ui-tabs-loading")||this.running||o&&!n.collapsible||this._trigger("beforeActivate",t,l)===!1)return;n.active=u?!1:this.tabs.index(s),this.active=o?e():s,this.xhr&&this.xhr.abort(),!f.length&&!a.length&&e.error("jQuery UI Tabs: Mismatching fragment identifier."),a.length&&this.load(this.tabs.index(s),t),this._toggle(t,l)},_toggle:function(t,n){function o(){r.running=!1,r._trigger("activate",t,n)}function u(){n.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),i.length&&r.options.show?r._show(i,r.options.show,o):(i.show(),o())}var r=this,i=n.newPanel,s=n.oldPanel;this.running=!0,s.length&&this.options.hide?this._hide(s,this.options.hide,function(){n.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),u()}):(n.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),s.hide(),u()),s.attr({"aria-expanded":"false","aria-hidden":"true"}),n.oldTab.attr("aria-selected","false"),i.length&&s.length?n.oldTab.attr("tabIndex",-1):i.length&&this.tabs.filter(function(){return e(this).attr("tabIndex")===0}).attr("tabIndex",-1),i.attr({"aria-expanded":"true","aria-hidden":"false"}),n.newTab.attr({"aria-selected":"true",tabIndex:0})},_activate:function(t){var n,r=this._findActive(t);if(r[0]===this.active[0])return;r.length||(r=this.active),n=r.find(".ui-tabs-anchor")[0],this._eventHandler({target:n,currentTarget:n,preventDefault:e.noop})},_findActive:function(t){return t===!1?e():this.tabs.eq(t)},_getIndex:function(e){return typeof e=="string"&&(e=this.anchors.index(this.anchors.filter("[href$='"+e+"']"))),e},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeData("href.tabs").removeData("load.tabs").removeUniqueId(),this.tabs.add(this.panels).each(function(){e.data(this,"ui-tabs-destroy")?e(this).remove():e(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var t=e(this),n=t.data("ui-tabs-aria-controls");n?t.attr("aria-controls",n):t.removeAttr("aria-controls")}),this.panels.show(),this.options.heightStyle!=="content"&&this.panels.css("height","")},enable:function(n){var r=this.options.disabled;if(r===!1)return;n===t?r=!1:(n=this._getIndex(n),e.isArray(r)?r=e.map(r,function(e){return e!==n?e:null}):r=e.map(this.tabs,function(e,t){return t!==n?t:null})),this._setupDisabled(r)},disable:function(n){var r=this.options.disabled;if(r===!0)return;if(n===t)r=!0;else{n=this._getIndex(n);if(e.inArray(n,r)!==-1)return;e.isArray(r)?r=e.merge([n],r).sort():r=[n]}this._setupDisabled(r)},load:function(t,n){t=this._getIndex(t);var r=this,i=this.tabs.eq(t),o=i.find(".ui-tabs-anchor"),u=this._getPanelForTab(i),a={tab:i,panel:u};if(s(o[0]))return;this.xhr=e.ajax(this._ajaxSettings(o,n,a)),this.xhr&&this.xhr.statusText!=="canceled"&&(i.addClass("ui-tabs-loading"),u.attr("aria-busy","true"),this.xhr.success(function(e){setTimeout(function(){u.html(e),r._trigger("load",n,a)},1)}).complete(function(e,t){setTimeout(function(){t==="abort"&&r.panels.stop(!1,!0),i.removeClass("ui-tabs-loading"),u.removeAttr("aria-busy"),e===r.xhr&&delete r.xhr},1)}))},_ajaxSettings:function(t,n,r){var i=this;return{url:t.attr("href"),beforeSend:function(t,s){return i._trigger("beforeLoad",n,e.extend({jqXHR:t,ajaxSettings:s},r))}}},_getPanelForTab:function(t){var n=e(t).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+n))}}),e.uiBackCompat!==!1&&(e.ui.tabs.prototype._ui=function(e,t){return{tab:e,panel:t,index:this.anchors.index(e)}},e.widget("ui.tabs",e.ui.tabs,{url:function(e,t){this.anchors.eq(e).attr("href",t)}}),e.widget("ui.tabs",e.ui.tabs,{options:{ajaxOptions:null,cache:!1},_create:function(){this._super();var t=this;this._on({tabsbeforeload:function(n,r){if(e.data(r.tab[0],"cache.tabs")){n.preventDefault();return}r.jqXHR.success(function(){t.options.cache&&e.data(r.tab[0],"cache.tabs",!0)})}})},_ajaxSettings:function(t,n,r){var i=this.options.ajaxOptions;return e.extend({},i,{error:function(e,t){try{i.error(e,t,r.tab.closest("li").index(),r.tab[0])}catch(n){}}},this._superApply(arguments))},_setOption:function(e,t){e==="cache"&&t===!1&&this.anchors.removeData("cache.tabs"),this._super(e,t)},_destroy:function(){this.anchors.removeData("cache.tabs"),this._super()},url:function(e){this.anchors.eq(e).removeData("cache.tabs"),this._superApply(arguments)}}),e.widget("ui.tabs",e.ui.tabs,{abort:function(){this.xhr&&this.xhr.abort()}}),e.widget("ui.tabs",e.ui.tabs,{options:{spinner:"Loading…"},_create:function(){this._super(),this._on({tabsbeforeload:function(e,t){if(e.target!==this.element[0]||!this.options.spinner)return;var n=t.tab.find("span"),r=n.html();n.html(this.options.spinner),t.jqXHR.complete(function(){n.html(r)})}})}}),e.widget("ui.tabs",e.ui.tabs,{options:{enable:null,disable:null},enable:function(t){var n=this.options,r;if(t&&n.disabled===!0||e.isArray(n.disabled)&&e.inArray(t,n.disabled)!==-1)r=!0;this._superApply(arguments),r&&this._trigger("enable",null,this._ui(this.anchors[t],this.panels[t]))},disable:function(t){var n=this.options,r;if(t&&n.disabled===!1||e.isArray(n.disabled)&&e.inArray(t,n.disabled)===-1)r=!0;this._superApply(arguments),r&&this._trigger("disable",null,this._ui(this.anchors[t],this.panels[t]))}}),e.widget("ui.tabs",e.ui.tabs,{options:{add:null,remove:null,tabTemplate:"
  • #{label}
  • "},add:function(n,r,i){i===t&&(i=this.anchors.length);var s,o,u=this.options,a=e(u.tabTemplate.replace(/#\{href\}/g,n).replace(/#\{label\}/g,r)),f=n.indexOf("#")?this._tabId(a):n.replace("#","");return a.addClass("ui-state-default ui-corner-top").data("ui-tabs-destroy",!0),a.attr("aria-controls",f),s=i>=this.tabs.length,o=this.element.find("#"+f),o.length||(o=this._createPanel(f),s?i>0?o.insertAfter(this.panels.eq(-1)):o.appendTo(this.element):o.insertBefore(this.panels[i])),o.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").hide(),s?a.appendTo(this.tablist):a.insertBefore(this.tabs[i]),u.disabled=e.map(u.disabled,function(e){return e>=i?++e:e}),this.refresh(),this.tabs.length===1&&u.active===!1&&this.option("active",0),this._trigger("add",null,this._ui(this.anchors[i],this.panels[i])),this},remove:function(t){t=this._getIndex(t);var n=this.options,r=this.tabs.eq(t).remove(),i=this._getPanelForTab(r).remove();return r.hasClass("ui-tabs-active")&&this.anchors.length>2&&this._activate(t+(t+1=t?--e:e}),this.refresh(),this._trigger("remove",null,this._ui(r.find("a")[0],i[0])),this}}),e.widget("ui.tabs",e.ui.tabs,{length:function(){return this.anchors.length}}),e.widget("ui.tabs",e.ui.tabs,{options:{idPrefix:"ui-tabs-"},_tabId:function(t){var n=t.is("li")?t.find("a[href]"):t;return n=n[0],e(n).closest("li").attr("aria-controls")||n.title&&n.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF\-]/g,"")||this.options.idPrefix+i()}}),e.widget("ui.tabs",e.ui.tabs,{options:{panelTemplate:"
    "},_createPanel:function(t){return e(this.options.panelTemplate).attr("id",t).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)}}),e.widget("ui.tabs",e.ui.tabs,{_create:function(){var e=this.options;e.active===null&&e.selected!==t&&(e.active=e.selected===-1?!1:e.selected),this._super(),e.selected=e.active,e.selected===!1&&(e.selected=-1)},_setOption:function(e,t){if(e!=="selected")return this._super(e,t);var n=this.options;this._super("active",t===-1?!1:t),n.selected=n.active,n.selected===!1&&(n.selected=-1)},_eventHandler:function(){this._superApply(arguments),this.options.selected=this.options.active,this.options.selected===!1&&(this.options.selected=-1)}}),e.widget("ui.tabs",e.ui.tabs,{options:{show:null,select:null},_create:function(){this._super(),this.options.active!==!1&&this._trigger("show",null,this._ui(this.active.find(".ui-tabs-anchor")[0],this._getPanelForTab(this.active)[0]))},_trigger:function(e,t,n){var r,i,s=this._superApply(arguments);return s?(e==="beforeActivate"?(r=n.newTab.length?n.newTab:n.oldTab,i=n.newPanel.length?n.newPanel:n.oldPanel,s=this._super("select",t,{tab:r.find(".ui-tabs-anchor")[0],panel:i[0],index:r.closest("li").index()})):e==="activate"&&n.newTab.length&&(s=this._super("show",t,{tab:n.newTab.find(".ui-tabs-anchor")[0],panel:n.newPanel[0],index:n.newTab.closest("li").index()})),s):!1}}),e.widget("ui.tabs",e.ui.tabs,{select:function(e){e=this._getIndex(e);if(e===-1){if(!this.options.collapsible||this.options.selected===-1)return;e=this.options.selected}this.anchors.eq(e).trigger(this.options.event+this.eventNamespace)}}),function(){var t=0;e.widget("ui.tabs",e.ui.tabs,{options:{cookie:null},_create:function(){var e=this.options,t;e.active==null&&e.cookie&&(t=parseInt(this._cookie(),10),t===-1&&(t=!1),e.active=t),this._super()},_cookie:function(n){var r=[this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+ ++t)];return arguments.length&&(r.push(n===!1?-1:n),r.push(this.options.cookie)),e.cookie.apply(null,r)},_refresh:function(){this._super(),this.options.cookie&&this._cookie(this.options.active,this.options.cookie)},_eventHandler:function(){this._superApply(arguments),this.options.cookie&&this._cookie(this.options.active,this.options.cookie)},_destroy:function(){this._super(),this.options.cookie&&this._cookie(null,this.options.cookie)}})}(),e.widget("ui.tabs",e.ui.tabs,{_trigger:function(t,n,r){var i=e.extend({},r);return t==="load"&&(i.panel=i.panel[0],i.tab=i.tab.find(".ui-tabs-anchor")[0]),this._super(t,n,i)}}),e.widget("ui.tabs",e.ui.tabs,{options:{fx:null},_getFx:function(){var t,n,r=this.options.fx;return r&&(e.isArray(r)?(t=r[0],n=r[1]):t=n=r),r?{show:n,hide:t}:null},_toggle:function(e,t){function o(){n.running=!1,n._trigger("activate",e,t)}function u(){t.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),r.length&&s.show?r.animate(s.show,s.show.duration,function(){o()}):(r.show(),o())}var n=this,r=t.newPanel,i=t.oldPanel,s=this._getFx();if(!s)return this._super(e,t);n.running=!0,i.length&&s.hide?i.animate(s.hide,s.hide.duration,function(){t.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),u()}):(t.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),i.hide(),u())}}))})(jQuery);(function(e){function n(t,n){var r=(t.attr("aria-describedby")||"").split(/\s+/);r.push(n),t.data("ui-tooltip-id",n).attr("aria-describedby",e.trim(r.join(" ")))}function r(t){var n=t.data("ui-tooltip-id"),r=(t.attr("aria-describedby")||"").split(/\s+/),i=e.inArray(n,r);i!==-1&&r.splice(i,1),t.removeData("ui-tooltip-id"),r=e.trim(r.join(" ")),r?t.attr("aria-describedby",r):t.removeAttr("aria-describedby")}var t=0;e.widget("ui.tooltip",{version:"1.9.2",options:{content:function(){return e(this).attr("title")},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable()},_setOption:function(t,n){var r=this;if(t==="disabled"){this[n?"_disable":"_enable"](),this.options[t]=n;return}this._super(t,n),t==="content"&&e.each(this.tooltips,function(e,t){r._updateContent(t)})},_disable:function(){var t=this;e.each(this.tooltips,function(n,r){var i=e.Event("blur");i.target=i.currentTarget=r[0],t.close(i,!0)}),this.element.find(this.options.items).andSelf().each(function(){var t=e(this);t.is("[title]")&&t.data("ui-tooltip-title",t.attr("title")).attr("title","")})},_enable:function(){this.element.find(this.options.items).andSelf().each(function(){var t=e(this);t.data("ui-tooltip-title")&&t.attr("title",t.data("ui-tooltip-title"))})},open:function(t){var n=this,r=e(t?t.target:this.element).closest(this.options.items);if(!r.length||r.data("ui-tooltip-id"))return;r.attr("title")&&r.data("ui-tooltip-title",r.attr("title")),r.data("ui-tooltip-open",!0),t&&t.type==="mouseover"&&r.parents().each(function(){var t=e(this),r;t.data("ui-tooltip-open")&&(r=e.Event("blur"),r.target=r.currentTarget=this,n.close(r,!0)),t.attr("title")&&(t.uniqueId(),n.parents[this.id]={element:this,title:t.attr("title")},t.attr("title",""))}),this._updateContent(r,t)},_updateContent:function(e,t){var n,r=this.options.content,i=this,s=t?t.type:null;if(typeof r=="string")return this._open(t,e,r);n=r.call(e[0],function(n){if(!e.data("ui-tooltip-open"))return;i._delay(function(){t&&(t.type=s),this._open(t,e,n)})}),n&&this._open(t,e,n)},_open:function(t,r,i){function f(e){a.of=e;if(s.is(":hidden"))return;s.position(a)}var s,o,u,a=e.extend({},this.options.position);if(!i)return;s=this._find(r);if(s.length){s.find(".ui-tooltip-content").html(i);return}r.is("[title]")&&(t&&t.type==="mouseover"?r.attr("title",""):r.removeAttr("title")),s=this._tooltip(r),n(r,s.attr("id")),s.find(".ui-tooltip-content").html(i),this.options.track&&t&&/^mouse/.test(t.type)?(this._on(this.document,{mousemove:f}),f(t)):s.position(e.extend({of:r},this.options.position)),s.hide(),this._show(s,this.options.show),this.options.show&&this.options.show.delay&&(u=setInterval(function(){s.is(":visible")&&(f(a.of),clearInterval(u))},e.fx.interval)),this._trigger("open",t,{tooltip:s}),o={keyup:function(t){if(t.keyCode===e.ui.keyCode.ESCAPE){var n=e.Event(t);n.currentTarget=r[0],this.close(n,!0)}},remove:function(){this._removeTooltip(s)}};if(!t||t.type==="mouseover")o.mouseleave="close";if(!t||t.type==="focusin")o.focusout="close";this._on(!0,r,o)},close:function(t){var n=this,i=e(t?t.currentTarget:this.element),s=this._find(i);if(this.closing)return;i.data("ui-tooltip-title")&&i.attr("title",i.data("ui-tooltip-title")),r(i),s.stop(!0),this._hide(s,this.options.hide,function(){n._removeTooltip(e(this))}),i.removeData("ui-tooltip-open"),this._off(i,"mouseleave focusout keyup"),i[0]!==this.element[0]&&this._off(i,"remove"),this._off(this.document,"mousemove"),t&&t.type==="mouseleave"&&e.each(this.parents,function(t,r){e(r.element).attr("title",r.title),delete n.parents[t]}),this.closing=!0,this._trigger("close",t,{tooltip:s}),this.closing=!1},_tooltip:function(n){var r="ui-tooltip-"+t++,i=e("
    ").attr({id:r,role:"tooltip"}).addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||""));return e("
    ").addClass("ui-tooltip-content").appendTo(i),i.appendTo(this.document[0].body),e.fn.bgiframe&&i.bgiframe(),this.tooltips[r]=n,i},_find:function(t){var n=t.data("ui-tooltip-id");return n?e("#"+n):e()},_removeTooltip:function(e){e.remove(),delete this.tooltips[e.attr("id")]},_destroy:function(){var t=this;e.each(this.tooltips,function(n,r){var i=e.Event("blur");i.target=i.currentTarget=r[0],t.close(i,!0),e("#"+n).remove(),r.data("ui-tooltip-title")&&(r.attr("title",r.data("ui-tooltip-title")),r.removeData("ui-tooltip-title"))})}})})(jQuery);jQuery.effects||function(e,t){var n=e.uiBackCompat!==!1,r="ui-effects-";e.effects={effect:{}},function(t,n){function p(e,t,n){var r=a[t.type]||{};return e==null?n||!t.def?null:t.def:(e=r.floor?~~e:parseFloat(e),isNaN(e)?t.def:r.mod?(e+r.mod)%r.mod:0>e?0:r.max")[0],c,h=t.each;l.style.cssText="background-color:rgba(1,1,1,.5)",f.rgba=l.style.backgroundColor.indexOf("rgba")>-1,h(u,function(e,t){t.cache="_"+e,t.props.alpha={idx:3,type:"percent",def:1}}),o.fn=t.extend(o.prototype,{parse:function(r,i,s,a){if(r===n)return this._rgba=[null,null,null,null],this;if(r.jquery||r.nodeType)r=t(r).css(i),i=n;var f=this,l=t.type(r),v=this._rgba=[];i!==n&&(r=[r,i,s,a],l="array");if(l==="string")return this.parse(d(r)||c._default);if(l==="array")return h(u.rgba.props,function(e,t){v[t.idx]=p(r[t.idx],t)}),this;if(l==="object")return r instanceof o?h(u,function(e,t){r[t.cache]&&(f[t.cache]=r[t.cache].slice())}):h(u,function(t,n){var i=n.cache;h(n.props,function(e,t){if(!f[i]&&n.to){if(e==="alpha"||r[e]==null)return;f[i]=n.to(f._rgba)}f[i][t.idx]=p(r[e],t,!0)}),f[i]&&e.inArray(null,f[i].slice(0,3))<0&&(f[i][3]=1,n.from&&(f._rgba=n.from(f[i])))}),this},is:function(e){var t=o(e),n=!0,r=this;return h(u,function(e,i){var s,o=t[i.cache];return o&&(s=r[i.cache]||i.to&&i.to(r._rgba)||[],h(i.props,function(e,t){if(o[t.idx]!=null)return n=o[t.idx]===s[t.idx],n})),n}),n},_space:function(){var e=[],t=this;return h(u,function(n,r){t[r.cache]&&e.push(n)}),e.pop()},transition:function(e,t){var n=o(e),r=n._space(),i=u[r],s=this.alpha()===0?o("transparent"):this,f=s[i.cache]||i.to(s._rgba),l=f.slice();return n=n[i.cache],h(i.props,function(e,r){var i=r.idx,s=f[i],o=n[i],u=a[r.type]||{};if(o===null)return;s===null?l[i]=o:(u.mod&&(o-s>u.mod/2?s+=u.mod:s-o>u.mod/2&&(s-=u.mod)),l[i]=p((o-s)*t+s,r))}),this[r](l)},blend:function(e){if(this._rgba[3]===1)return this;var n=this._rgba.slice(),r=n.pop(),i=o(e)._rgba;return o(t.map(n,function(e,t){return(1-r)*i[t]+r*e}))},toRgbaString:function(){var e="rgba(",n=t.map(this._rgba,function(e,t){return e==null?t>2?1:0:e});return n[3]===1&&(n.pop(),e="rgb("),e+n.join()+")"},toHslaString:function(){var e="hsla(",n=t.map(this.hsla(),function(e,t){return e==null&&(e=t>2?1:0),t&&t<3&&(e=Math.round(e*100)+"%"),e});return n[3]===1&&(n.pop(),e="hsl("),e+n.join()+")"},toHexString:function(e){var n=this._rgba.slice(),r=n.pop();return e&&n.push(~~(r*255)),"#"+t.map(n,function(e){return e=(e||0).toString(16),e.length===1?"0"+e:e}).join("")},toString:function(){return this._rgba[3]===0?"transparent":this.toRgbaString()}}),o.fn.parse.prototype=o.fn,u.hsla.to=function(e){if(e[0]==null||e[1]==null||e[2]==null)return[null,null,null,e[3]];var t=e[0]/255,n=e[1]/255,r=e[2]/255,i=e[3],s=Math.max(t,n,r),o=Math.min(t,n,r),u=s-o,a=s+o,f=a*.5,l,c;return o===s?l=0:t===s?l=60*(n-r)/u+360:n===s?l=60*(r-t)/u+120:l=60*(t-n)/u+240,f===0||f===1?c=f:f<=.5?c=u/a:c=u/(2-a),[Math.round(l)%360,c,f,i==null?1:i]},u.hsla.from=function(e){if(e[0]==null||e[1]==null||e[2]==null)return[null,null,null,e[3]];var t=e[0]/360,n=e[1],r=e[2],i=e[3],s=r<=.5?r*(1+n):r+n-r*n,o=2*r-s;return[Math.round(v(o,s,t+1/3)*255),Math.round(v(o,s,t)*255),Math.round(v(o,s,t-1/3)*255),i]},h(u,function(e,r){var s=r.props,u=r.cache,a=r.to,f=r.from;o.fn[e]=function(e){a&&!this[u]&&(this[u]=a(this._rgba));if(e===n)return this[u].slice();var r,i=t.type(e),l=i==="array"||i==="object"?e:arguments,c=this[u].slice();return h(s,function(e,t){var n=l[i==="object"?e:t.idx];n==null&&(n=c[t.idx]),c[t.idx]=p(n,t)}),f?(r=o(f(c)),r[u]=c,r):o(c)},h(s,function(n,r){if(o.fn[n])return;o.fn[n]=function(s){var o=t.type(s),u=n==="alpha"?this._hsla?"hsla":"rgba":e,a=this[u](),f=a[r.idx],l;return o==="undefined"?f:(o==="function"&&(s=s.call(this,f),o=t.type(s)),s==null&&r.empty?this:(o==="string"&&(l=i.exec(s),l&&(s=f+parseFloat(l[2])*(l[1]==="+"?1:-1))),a[r.idx]=s,this[u](a)))}})}),h(r,function(e,n){t.cssHooks[n]={set:function(e,r){var i,s,u="";if(t.type(r)!=="string"||(i=d(r))){r=o(i||r);if(!f.rgba&&r._rgba[3]!==1){s=n==="backgroundColor"?e.parentNode:e;while((u===""||u==="transparent")&&s&&s.style)try{u=t.css(s,"backgroundColor"),s=s.parentNode}catch(a){}r=r.blend(u&&u!=="transparent"?u:"_default")}r=r.toRgbaString()}try{e.style[n]=r}catch(l){}}},t.fx.step[n]=function(e){e.colorInit||(e.start=o(e.elem,n),e.end=o(e.end),e.colorInit=!0),t.cssHooks[n].set(e.elem,e.start.transition(e.end,e.pos))}}),t.cssHooks.borderColor={expand:function(e){var t={};return h(["Top","Right","Bottom","Left"],function(n,r){t["border"+r+"Color"]=e}),t}},c=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(){var t=this.ownerDocument.defaultView?this.ownerDocument.defaultView.getComputedStyle(this,null):this.currentStyle,n={},r,i;if(t&&t.length&&t[0]&&t[t[0]]){i=t.length;while(i--)r=t[i],typeof t[r]=="string"&&(n[e.camelCase(r)]=t[r])}else for(r in t)typeof t[r]=="string"&&(n[r]=t[r]);return n}function s(t,n){var i={},s,o;for(s in n)o=n[s],t[s]!==o&&!r[s]&&(e.fx.step[s]||!isNaN(parseFloat(o)))&&(i[s]=o);return i}var n=["add","remove","toggle"],r={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};e.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(t,n){e.fx.step[n]=function(e){if(e.end!=="none"&&!e.setAttr||e.pos===1&&!e.setAttr)jQuery.style(e.elem,n,e.end),e.setAttr=!0}}),e.effects.animateClass=function(t,r,o,u){var a=e.speed(r,o,u);return this.queue(function(){var r=e(this),o=r.attr("class")||"",u,f=a.children?r.find("*").andSelf():r;f=f.map(function(){var t=e(this);return{el:t,start:i.call(this)}}),u=function(){e.each(n,function(e,n){t[n]&&r[n+"Class"](t[n])})},u(),f=f.map(function(){return this.end=i.call(this.el[0]),this.diff=s(this.start,this.end),this}),r.attr("class",o),f=f.map(function(){var t=this,n=e.Deferred(),r=jQuery.extend({},a,{queue:!1,complete:function(){n.resolve(t)}});return this.el.animate(this.diff,r),n.promise()}),e.when.apply(e,f.get()).done(function(){u(),e.each(arguments,function(){var t=this.el;e.each(this.diff,function(e){t.css(e,"")})}),a.complete.call(r[0])})})},e.fn.extend({_addClass:e.fn.addClass,addClass:function(t,n,r,i){return n?e.effects.animateClass.call(this,{add:t},n,r,i):this._addClass(t)},_removeClass:e.fn.removeClass,removeClass:function(t,n,r,i){return n?e.effects.animateClass.call(this,{remove:t},n,r,i):this._removeClass(t)},_toggleClass:e.fn.toggleClass,toggleClass:function(n,r,i,s,o){return typeof r=="boolean"||r===t?i?e.effects.animateClass.call(this,r?{add:n}:{remove:n},i,s,o):this._toggleClass(n,r):e.effects.animateClass.call(this,{toggle:n},r,i,s)},switchClass:function(t,n,r,i,s){return e.effects.animateClass.call(this,{add:n,remove:t},r,i,s)}})}(),function(){function i(t,n,r,i){e.isPlainObject(t)&&(n=t,t=t.effect),t={effect:t},n==null&&(n={}),e.isFunction(n)&&(i=n,r=null,n={});if(typeof n=="number"||e.fx.speeds[n])i=r,r=n,n={};return e.isFunction(r)&&(i=r,r=null),n&&e.extend(t,n),r=r||n.duration,t.duration=e.fx.off?0:typeof r=="number"?r:r in e.fx.speeds?e.fx.speeds[r]:e.fx.speeds._default,t.complete=i||n.complete,t}function s(t){return!t||typeof t=="number"||e.fx.speeds[t]?!0:typeof t=="string"&&!e.effects.effect[t]?n&&e.effects[t]?!1:!0:!1}e.extend(e.effects,{version:"1.9.2",save:function(e,t){for(var n=0;n
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),i={width:t.width(),height:t.height()},s=document.activeElement;try{s.id}catch(o){s=document.body}return t.wrap(r),(t[0]===s||e.contains(t[0],s))&&e(s).focus(),r=t.parent(),t.css("position")==="static"?(r.css({position:"relative"}),t.css({position:"relative"})):(e.extend(n,{position:t.css("position"),zIndex:t.css("z-index")}),e.each(["top","left","bottom","right"],function(e,r){n[r]=t.css(r),isNaN(parseInt(n[r],10))&&(n[r]="auto")}),t.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),t.css(i),r.css(n).show()},removeWrapper:function(t){var n=document.activeElement;return t.parent().is(".ui-effects-wrapper")&&(t.parent().replaceWith(t),(t[0]===n||e.contains(t[0],n))&&e(n).focus()),t},setTransition:function(t,n,r,i){return i=i||{},e.each(n,function(e,n){var s=t.cssUnit(n);s[0]>0&&(i[n]=s[0]*r+s[1])}),i}}),e.fn.extend({effect:function(){function a(n){function u(){e.isFunction(i)&&i.call(r[0]),e.isFunction(n)&&n()}var r=e(this),i=t.complete,s=t.mode;(r.is(":hidden")?s==="hide":s==="show")?u():o.call(r[0],t,u)}var t=i.apply(this,arguments),r=t.mode,s=t.queue,o=e.effects.effect[t.effect],u=!o&&n&&e.effects[t.effect];return e.fx.off||!o&&!u?r?this[r](t.duration,t.complete):this.each(function(){t.complete&&t.complete.call(this)}):o?s===!1?this.each(a):this.queue(s||"fx",a):u.call(this,{options:t,duration:t.duration,callback:t.complete,mode:t.mode})},_show:e.fn.show,show:function(e){if(s(e))return this._show.apply(this,arguments);var t=i.apply(this,arguments);return t.mode="show",this.effect.call(this,t)},_hide:e.fn.hide,hide:function(e){if(s(e))return this._hide.apply(this,arguments);var t=i.apply(this,arguments);return t.mode="hide",this.effect.call(this,t)},__toggle:e.fn.toggle,toggle:function(t){if(s(t)||typeof t=="boolean"||e.isFunction(t))return this.__toggle.apply(this,arguments);var n=i.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)},cssUnit:function(t){var n=this.css(t),r=[];return e.each(["em","px","%","pt"],function(e,t){n.indexOf(t)>0&&(r=[parseFloat(n),t])}),r}})}(),function(){var t={};e.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,n){t[n]=function(t){return Math.pow(t,e+2)}}),e.extend(t,{Sine:function(e){return 1-Math.cos(e*Math.PI/2)},Circ:function(e){return 1-Math.sqrt(1-e*e)},Elastic:function(e){return e===0||e===1?e:-Math.pow(2,8*(e-1))*Math.sin(((e-1)*80-7.5)*Math.PI/15)},Back:function(e){return e*e*(3*e-2)},Bounce:function(e){var t,n=4;while(e<((t=Math.pow(2,--n))-1)/11);return 1/Math.pow(4,3-n)-7.5625*Math.pow((t*3-2)/22-e,2)}}),e.each(t,function(t,n){e.easing["easeIn"+t]=n,e.easing["easeOut"+t]=function(e){return 1-n(1-e)},e.easing["easeInOut"+t]=function(e){return e<.5?n(e*2)/2:1-n(e*-2+2)/2}})}()}(jQuery);(function(e,t){var n=/up|down|vertical/,r=/up|left|vertical|horizontal/;e.effects.effect.blind=function(t,i){var s=e(this),o=["position","top","bottom","left","right","height","width"],u=e.effects.setMode(s,t.mode||"hide"),a=t.direction||"up",f=n.test(a),l=f?"height":"width",c=f?"top":"left",h=r.test(a),p={},d=u==="show",v,m,g;s.parent().is(".ui-effects-wrapper")?e.effects.save(s.parent(),o):e.effects.save(s,o),s.show(),v=e.effects.createWrapper(s).css({overflow:"hidden"}),m=v[l](),g=parseFloat(v.css(c))||0,p[l]=d?m:0,h||(s.css(f?"bottom":"right",0).css(f?"top":"left","auto").css({position:"absolute"}),p[c]=d?g:m+g),d&&(v.css(l,0),h||v.css(c,g+m)),v.animate(p,{duration:t.duration,easing:t.easing,queue:!1,complete:function(){u==="hide"&&s.hide(),e.effects.restore(s,o),e.effects.removeWrapper(s),i()}})}})(jQuery);(function(e,t){e.effects.effect.bounce=function(t,n){var r=e(this),i=["position","top","bottom","left","right","height","width"],s=e.effects.setMode(r,t.mode||"effect"),o=s==="hide",u=s==="show",a=t.direction||"up",f=t.distance,l=t.times||5,c=l*2+(u||o?1:0),h=t.duration/c,p=t.easing,d=a==="up"||a==="down"?"top":"left",v=a==="up"||a==="left",m,g,y,b=r.queue(),w=b.length;(u||o)&&i.push("opacity"),e.effects.save(r,i),r.show(),e.effects.createWrapper(r),f||(f=r[d==="top"?"outerHeight":"outerWidth"]()/3),u&&(y={opacity:1},y[d]=0,r.css("opacity",0).css(d,v?-f*2:f*2).animate(y,h,p)),o&&(f/=Math.pow(2,l-1)),y={},y[d]=0;for(m=0;m1&&b.splice.apply(b,[1,0].concat(b.splice(w,c+1))),r.dequeue()}})(jQuery);(function(e,t){e.effects.effect.clip=function(t,n){var r=e(this),i=["position","top","bottom","left","right","height","width"],s=e.effects.setMode(r,t.mode||"hide"),o=s==="show",u=t.direction||"vertical",a=u==="vertical",f=a?"height":"width",l=a?"top":"left",c={},h,p,d;e.effects.save(r,i),r.show(),h=e.effects.createWrapper(r).css({overflow:"hidden"}),p=r[0].tagName==="IMG"?h:r,d=p[f](),o&&(p.css(f,0),p.css(l,d/2)),c[f]=o?d:0,c[l]=o?0:d/2,p.animate(c,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){o||r.hide(),e.effects.restore(r,i),e.effects.removeWrapper(r),n()}})}})(jQuery);(function(e,t){e.effects.effect.drop=function(t,n){var r=e(this),i=["position","top","bottom","left","right","opacity","height","width"],s=e.effects.setMode(r,t.mode||"hide"),o=s==="show",u=t.direction||"left",a=u==="up"||u==="down"?"top":"left",f=u==="up"||u==="left"?"pos":"neg",l={opacity:o?1:0},c;e.effects.save(r,i),r.show(),e.effects.createWrapper(r),c=t.distance||r[a==="top"?"outerHeight":"outerWidth"](!0)/2,o&&r.css("opacity",0).css(a,f==="pos"?-c:c),l[a]=(o?f==="pos"?"+=":"-=":f==="pos"?"-=":"+=")+c,r.animate(l,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){s==="hide"&&r.hide(),e.effects.restore(r,i),e.effects.removeWrapper(r),n()}})}})(jQuery);(function(e,t){e.effects.effect.explode=function(t,n){function y(){c.push(this),c.length===r*i&&b()}function b(){s.css({visibility:"visible"}),e(c).remove(),u||s.hide(),n()}var r=t.pieces?Math.round(Math.sqrt(t.pieces)):3,i=r,s=e(this),o=e.effects.setMode(s,t.mode||"hide"),u=o==="show",a=s.show().css("visibility","hidden").offset(),f=Math.ceil(s.outerWidth()/i),l=Math.ceil(s.outerHeight()/r),c=[],h,p,d,v,m,g;for(h=0;h
    ").css({position:"absolute",visibility:"visible",left:-p*f,top:-h*l}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:f,height:l,left:d+(u?m*f:0),top:v+(u?g*l:0),opacity:u?0:1}).animate({left:d+(u?0:m*f),top:v+(u?0:g*l),opacity:u?1:0},t.duration||500,t.easing,y)}}})(jQuery);(function(e,t){e.effects.effect.fade=function(t,n){var r=e(this),i=e.effects.setMode(r,t.mode||"toggle");r.animate({opacity:i},{queue:!1,duration:t.duration,easing:t.easing,complete:n})}})(jQuery);(function(e,t){e.effects.effect.fold=function(t,n){var r=e(this),i=["position","top","bottom","left","right","height","width"],s=e.effects.setMode(r,t.mode||"hide"),o=s==="show",u=s==="hide",a=t.size||15,f=/([0-9]+)%/.exec(a),l=!!t.horizFirst,c=o!==l,h=c?["width","height"]:["height","width"],p=t.duration/2,d,v,m={},g={};e.effects.save(r,i),r.show(),d=e.effects.createWrapper(r).css({overflow:"hidden"}),v=c?[d.width(),d.height()]:[d.height(),d.width()],f&&(a=parseInt(f[1],10)/100*v[u?0:1]),o&&d.css(l?{height:0,width:a}:{height:a,width:0}),m[h[0]]=o?v[0]:a,g[h[1]]=o?v[1]:0,d.animate(m,p,t.easing).animate(g,p,t.easing,function(){u&&r.hide(),e.effects.restore(r,i),e.effects.removeWrapper(r),n()})}})(jQuery);(function(e,t){e.effects.effect.highlight=function(t,n){var r=e(this),i=["backgroundImage","backgroundColor","opacity"],s=e.effects.setMode(r,t.mode||"show"),o={backgroundColor:r.css("backgroundColor")};s==="hide"&&(o.opacity=0),e.effects.save(r,i),r.show().css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(o,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){s==="hide"&&r.hide(),e.effects.restore(r,i),n()}})}})(jQuery);(function(e,t){e.effects.effect.pulsate=function(t,n){var r=e(this),i=e.effects.setMode(r,t.mode||"show"),s=i==="show",o=i==="hide",u=s||i==="hide",a=(t.times||5)*2+(u?1:0),f=t.duration/a,l=0,c=r.queue(),h=c.length,p;if(s||!r.is(":visible"))r.css("opacity",0).show(),l=1;for(p=1;p1&&c.splice.apply(c,[1,0].concat(c.splice(h,a+1))),r.dequeue()}})(jQuery);(function(e,t){e.effects.effect.puff=function(t,n){var r=e(this),i=e.effects.setMode(r,t.mode||"hide"),s=i==="hide",o=parseInt(t.percent,10)||150,u=o/100,a={height:r.height(),width:r.width(),outerHeight:r.outerHeight(),outerWidth:r.outerWidth()};e.extend(t,{effect:"scale",queue:!1,fade:!0,mode:i,complete:n,percent:s?o:100,from:s?a:{height:a.height*u,width:a.width*u,outerHeight:a.outerHeight*u,outerWidth:a.outerWidth*u}}),r.effect(t)},e.effects.effect.scale=function(t,n){var r=e(this),i=e.extend(!0,{},t),s=e.effects.setMode(r,t.mode||"effect"),o=parseInt(t.percent,10)||(parseInt(t.percent,10)===0?0:s==="hide"?0:100),u=t.direction||"both",a=t.origin,f={height:r.height(),width:r.width(),outerHeight:r.outerHeight(),outerWidth:r.outerWidth()},l={y:u!=="horizontal"?o/100:1,x:u!=="vertical"?o/100:1};i.effect="size",i.queue=!1,i.complete=n,s!=="effect"&&(i.origin=a||["middle","center"],i.restore=!0),i.from=t.from||(s==="show"?{height:0,width:0,outerHeight:0,outerWidth:0}:f),i.to={height:f.height*l.y,width:f.width*l.x,outerHeight:f.outerHeight*l.y,outerWidth:f.outerWidth*l.x},i.fade&&(s==="show"&&(i.from.opacity=0,i.to.opacity=1),s==="hide"&&(i.from.opacity=1,i.to.opacity=0)),r.effect(i)},e.effects.effect.size=function(t,n){var r,i,s,o=e(this),u=["position","top","bottom","left","right","width","height","overflow","opacity"],a=["position","top","bottom","left","right","overflow","opacity"],f=["width","height","overflow"],l=["fontSize"],c=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],h=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=e.effects.setMode(o,t.mode||"effect"),d=t.restore||p!=="effect",v=t.scale||"both",m=t.origin||["middle","center"],g=o.css("position"),y=d?u:a,b={height:0,width:0,outerHeight:0,outerWidth:0};p==="show"&&o.show(),r={height:o.height(),width:o.width(),outerHeight:o.outerHeight(),outerWidth:o.outerWidth()},t.mode==="toggle"&&p==="show"?(o.from=t.to||b,o.to=t.from||r):(o.from=t.from||(p==="show"?b:r),o.to=t.to||(p==="hide"?b:r)),s={from:{y:o.from.height/r.height,x:o.from.width/r.width},to:{y:o.to.height/r.height,x:o.to.width/r.width}};if(v==="box"||v==="both")s.from.y!==s.to.y&&(y=y.concat(c),o.from=e.effects.setTransition(o,c,s.from.y,o.from),o.to=e.effects.setTransition(o,c,s.to.y,o.to)),s.from.x!==s.to.x&&(y=y.concat(h),o.from=e.effects.setTransition(o,h,s.from.x,o.from),o.to=e.effects.setTransition(o,h,s.to.x,o.to));(v==="content"||v==="both")&&s.from.y!==s.to.y&&(y=y.concat(l).concat(f),o.from=e.effects.setTransition(o,l,s.from.y,o.from),o.to=e.effects.setTransition(o,l,s.to.y,o.to)),e.effects.save(o,y),o.show(),e.effects.createWrapper(o),o.css("overflow","hidden").css(o.from),m&&(i=e.effects.getBaseline(m,r),o.from.top=(r.outerHeight-o.outerHeight())*i.y,o.from.left=(r.outerWidth-o.outerWidth())*i.x,o.to.top=(r.outerHeight-o.to.outerHeight)*i.y,o.to.left=(r.outerWidth-o.to.outerWidth)*i.x),o.css(o.from);if(v==="content"||v==="both")c=c.concat(["marginTop","marginBottom"]).concat(l),h=h.concat(["marginLeft","marginRight"]),f=u.concat(c).concat(h),o.find("*[width]").each(function(){var n=e(this),r={height:n.height(),width:n.width(),outerHeight:n.outerHeight(),outerWidth:n.outerWidth()};d&&e.effects.save(n,f),n.from={height:r.height*s.from.y,width:r.width*s.from.x,outerHeight:r.outerHeight*s.from.y,outerWidth:r.outerWidth*s.from.x},n.to={height:r.height*s.to.y,width:r.width*s.to.x,outerHeight:r.height*s.to.y,outerWidth:r.width*s.to.x},s.from.y!==s.to.y&&(n.from=e.effects.setTransition(n,c,s.from.y,n.from),n.to=e.effects.setTransition(n,c,s.to.y,n.to)),s.from.x!==s.to.x&&(n.from=e.effects.setTransition(n,h,s.from.x,n.from),n.to=e.effects.setTransition(n,h,s.to.x,n.to)),n.css(n.from),n.animate(n.to,t.duration,t.easing,function(){d&&e.effects.restore(n,f)})});o.animate(o.to,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){o.to.opacity===0&&o.css("opacity",o.from.opacity),p==="hide"&&o.hide(),e.effects.restore(o,y),d||(g==="static"?o.css({position:"relative",top:o.to.top,left:o.to.left}):e.each(["top","left"],function(e,t){o.css(t,function(t,n){var r=parseInt(n,10),i=e?o.to.left:o.to.top;return n==="auto"?i+"px":r+i+"px"})})),e.effects.removeWrapper(o),n()}})}})(jQuery);(function(e,t){e.effects.effect.shake=function(t,n){var r=e(this),i=["position","top","bottom","left","right","height","width"],s=e.effects.setMode(r,t.mode||"effect"),o=t.direction||"left",u=t.distance||20,a=t.times||3,f=a*2+1,l=Math.round(t.duration/f),c=o==="up"||o==="down"?"top":"left",h=o==="up"||o==="left",p={},d={},v={},m,g=r.queue(),y=g.length;e.effects.save(r,i),r.show(),e.effects.createWrapper(r),p[c]=(h?"-=":"+=")+u,d[c]=(h?"+=":"-=")+u*2,v[c]=(h?"-=":"+=")+u*2,r.animate(p,l,t.easing);for(m=1;m1&&g.splice.apply(g,[1,0].concat(g.splice(y,f+1))),r.dequeue()}})(jQuery);(function(e,t){e.effects.effect.slide=function(t,n){var r=e(this),i=["position","top","bottom","left","right","width","height"],s=e.effects.setMode(r,t.mode||"show"),o=s==="show",u=t.direction||"left",a=u==="up"||u==="down"?"top":"left",f=u==="up"||u==="left",l,c={};e.effects.save(r,i),r.show(),l=t.distance||r[a==="top"?"outerHeight":"outerWidth"](!0),e.effects.createWrapper(r).css({overflow:"hidden"}),o&&r.css(a,f?isNaN(l)?"-"+l:-l:l),c[a]=(o?f?"+=":"-=":f?"-=":"+=")+l,r.animate(c,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){s==="hide"&&r.hide(),e.effects.restore(r,i),e.effects.removeWrapper(r),n()}})}})(jQuery);(function(e,t){e.effects.effect.transfer=function(t,n){var r=e(this),i=e(t.to),s=i.css("position")==="fixed",o=e("body"),u=s?o.scrollTop():0,a=s?o.scrollLeft():0,f=i.offset(),l={top:f.top-u,left:f.left-a,height:i.innerHeight(),width:i.innerWidth()},c=r.offset(),h=e('
    ').appendTo(document.body).addClass(t.className).css({top:c.top-u,left:c.left-a,height:r.innerHeight(),width:r.innerWidth(),position:s?"fixed":"absolute"}).animate(l,t.duration,t.easing,function(){h.remove(),n()})}})(jQuery); + +/* JQuery UJS 2.0.3 */ +(function(a,b){var c=function(){var b=a(document).data("events");return b&&b.click&&a.grep(b.click,function(a){return a.namespace==="rails"}).length};if(c()){a.error("jquery-ujs has already been loaded!")}var d;a.rails=d={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with], button[data-disable-with], textarea[data-disable-with]",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input:file",linkDisableSelector:"a[data-disable-with]",CSRFProtection:function(b){var c=a('meta[name="csrf-token"]').attr("content");if(c)b.setRequestHeader("X-CSRF-Token",c)},fire:function(b,c,d){var e=a.Event(c);b.trigger(e,d);return e.result!==false},confirm:function(a){return confirm(a)},ajax:function(b){return a.ajax(b)},href:function(a){return a.attr("href")},handleRemote:function(c){var e,f,g,h,i,j,k,l;if(d.fire(c,"ajax:before")){h=c.data("cross-domain");i=h===b?null:h;j=c.data("with-credentials")||null;k=c.data("type")||a.ajaxSettings&&a.ajaxSettings.dataType;if(c.is("form")){e=c.attr("method");f=c.attr("action");g=c.serializeArray();var m=c.data("ujs:submit-button");if(m){g.push(m);c.data("ujs:submit-button",null)}}else if(c.is(d.inputChangeSelector)){e=c.data("method");f=c.data("url");g=c.serialize();if(c.data("params"))g=g+"&"+c.data("params")}else{e=c.data("method");f=d.href(c);g=c.data("params")||null}l={type:e||"GET",data:g,dataType:k,beforeSend:function(a,e){if(e.dataType===b){a.setRequestHeader("accept","*/*;q=0.5, "+e.accepts.script)}return d.fire(c,"ajax:beforeSend",[a,e])},success:function(a,b,d){c.trigger("ajax:success",[a,b,d])},complete:function(a,b){c.trigger("ajax:complete",[a,b])},error:function(a,b,d){c.trigger("ajax:error",[a,b,d])},xhrFields:{withCredentials:j},crossDomain:i};if(f){l.url=f}var n=d.ajax(l);c.trigger("ajax:send",n);return n}else{return false}},handleMethod:function(c){var e=d.href(c),f=c.data("method"),g=c.attr("target"),h=a("meta[name=csrf-token]").attr("content"),i=a("meta[name=csrf-param]").attr("content"),j=a('
    '),k='';if(i!==b&&h!==b){k+=''}if(g){j.attr("target",g)}j.hide().append(k).appendTo("body");j.submit()},disableFormElements:function(b){b.find(d.disableSelector).each(function(){var b=a(this),c=b.is("button")?"html":"val";b.data("ujs:enable-with",b[c]());b[c](b.data("disable-with"));b.prop("disabled",true)})},enableFormElements:function(b){b.find(d.enableSelector).each(function(){var b=a(this),c=b.is("button")?"html":"val";if(b.data("ujs:enable-with"))b[c](b.data("ujs:enable-with"));b.prop("disabled",false)})},allowAction:function(a){var b=a.data("confirm"),c=false,e;if(!b){return true}if(d.fire(a,"confirm")){c=d.confirm(b);e=d.fire(a,"confirm:complete",[c])}return c&&e},blankInputs:function(b,c,d){var e=a(),f,g,h=c||"input,textarea";b.find(h).each(function(){f=a(this);g=f.is(":checkbox,:radio")?f.is(":checked"):f.val();if(g==!!d){e=e.add(f)}});return e.length?e:false},nonBlankInputs:function(a,b){return d.blankInputs(a,b,true)},stopEverything:function(b){a(b.target).trigger("ujs:everythingStopped");b.stopImmediatePropagation();return false},callFormSubmitBindings:function(c,d){var e=c.data("events"),f=true;if(e!==b&&e["submit"]!==b){a.each(e["submit"],function(a,b){if(typeof b.handler==="function")return f=b.handler(d)})}return f},disableElement:function(a){a.data("ujs:enable-with",a.html());a.html(a.data("disable-with"));a.bind("click.railsDisable",function(a){return d.stopEverything(a)})},enableElement:function(a){if(a.data("ujs:enable-with")!==b){a.html(a.data("ujs:enable-with"));a.data("ujs:enable-with",false)}a.unbind("click.railsDisable")}};if(d.fire(a(document),"rails:attachBindings")){a.ajaxPrefilter(function(a,b,c){if(!a.crossDomain){d.CSRFProtection(c)}});a(document).delegate(d.linkDisableSelector,"ajax:complete",function(){d.enableElement(a(this))});a(document).delegate(d.linkClickSelector,"click.rails",function(c){var e=a(this),f=e.data("method"),g=e.data("params");if(!d.allowAction(e))return d.stopEverything(c);if(e.is(d.linkDisableSelector))d.disableElement(e);if(e.data("remote")!==b){if((c.metaKey||c.ctrlKey)&&(!f||f==="GET")&&!g){return true}if(d.handleRemote(e)===false){d.enableElement(e)}return false}else if(e.data("method")){d.handleMethod(e);return false}});a(document).delegate(d.inputChangeSelector,"change.rails",function(b){var c=a(this);if(!d.allowAction(c))return d.stopEverything(b);d.handleRemote(c);return false});a(document).delegate(d.formSubmitSelector,"submit.rails",function(c){var e=a(this),f=e.data("remote")!==b,g=d.blankInputs(e,d.requiredInputSelector),h=d.nonBlankInputs(e,d.fileInputSelector);if(!d.allowAction(e))return d.stopEverything(c);if(g&&e.attr("novalidate")==b&&d.fire(e,"ajax:aborted:required",[g])){return d.stopEverything(c)}if(f){if(h){setTimeout(function(){d.disableFormElements(e)},13);return d.fire(e,"ajax:aborted:file",[h])}if(!a.support.submitBubbles&&a().jquery<"1.7"&&d.callFormSubmitBindings(e,c)===false)return d.stopEverything(c);d.handleRemote(e);return false}else{setTimeout(function(){d.disableFormElements(e)},13)}});a(document).delegate(d.formInputClickSelector,"click.rails",function(b){var c=a(this);if(!d.allowAction(c))return d.stopEverything(b);var e=c.attr("name"),f=e?{name:e,value:c.val()}:null;c.closest("form").data("ujs:submit-button",f)});a(document).delegate(d.formSubmitSelector,"ajax:beforeSend.rails",function(b){if(this==b.target)d.disableFormElements(a(this))});a(document).delegate(d.formSubmitSelector,"ajax:complete.rails",function(b){if(this==b.target)d.enableFormElements(a(this))});a(function(){csrf_token=a("meta[name=csrf-token]").attr("content");csrf_param=a("meta[name=csrf-param]").attr("content");a('form input[name="'+csrf_param+'"]').val(csrf_token)})}})(jQuery) diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/jstoolbar/jstoolbar-textile.min.js --- a/public/javascripts/jstoolbar/jstoolbar-textile.min.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/jstoolbar/jstoolbar-textile.min.js Tue Jan 14 14:37:42 2014 +0000 @@ -1,1 +1,2 @@ -function jsToolBar(e){if(!document.createElement){return}if(!e){return}if(typeof document["selection"]=="undefined"&&typeof e["setSelectionRange"]=="undefined"){return}this.textarea=e;this.editor=document.createElement("div");this.editor.className="jstEditor";this.textarea.parentNode.insertBefore(this.editor,this.textarea);this.editor.appendChild(this.textarea);this.toolbar=document.createElement("div");this.toolbar.className="jstElements";this.editor.parentNode.insertBefore(this.toolbar,this.editor);if(this.editor.addEventListener&&navigator.appVersion.match(/\bMSIE\b/)){this.handle=document.createElement("div");this.handle.className="jstHandle";var t=this.resizeDragStart;var n=this;this.handle.addEventListener("mousedown",function(e){t.call(n,e)},false);window.addEventListener("unload",function(){var e=n.handle.parentNode.removeChild(n.handle);delete n.handle},false);this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling)}this.context=null;this.toolNodes={}}function jsButton(e,t,n,r){if(typeof jsToolBar.strings=="undefined"){this.title=e||null}else{this.title=jsToolBar.strings[e]||e||null}this.fn=t||function(){};this.scope=n||null;this.className=r||null}function jsSpace(e){this.id=e||null;this.width=null}function jsCombo(e,t,n,r,i){this.title=e||null;this.options=t||null;this.scope=n||null;this.fn=r||function(){};this.className=i||null}jsButton.prototype.draw=function(){if(!this.scope)return null;var e=document.createElement("button");e.setAttribute("type","button");e.tabIndex=200;if(this.className)e.className=this.className;e.title=this.title;var t=document.createElement("span");t.appendChild(document.createTextNode(this.title));e.appendChild(t);if(this.icon!=undefined){e.style.backgroundImage="url("+this.icon+")"}if(typeof this.fn=="function"){var n=this;e.onclick=function(){try{n.fn.apply(n.scope,arguments)}catch(e){}return false}}return e};jsSpace.prototype.draw=function(){var e=document.createElement("span");if(this.id)e.id=this.id;e.appendChild(document.createTextNode(String.fromCharCode(160)));e.className="jstSpacer";if(this.width)e.style.marginRight=this.width+"px";return e};jsCombo.prototype.draw=function(){if(!this.scope||!this.options)return null;var e=document.createElement("select");if(this.className)e.className=className;e.title=this.title;for(var t in this.options){var n=document.createElement("option");n.value=t;n.appendChild(document.createTextNode(this.options[t]));e.appendChild(n)}var r=this;e.onchange=function(){try{r.fn.call(r.scope,this.value)}catch(e){alert(e)}return false};return e};jsToolBar.prototype={base_url:"",mode:"wiki",elements:{},help_link:"",getMode:function(){return this.mode},setMode:function(e){this.mode=e||"wiki"},switchMode:function(e){e=e||"wiki";this.draw(e)},setHelpLink:function(e){this.help_link=e},button:function(e){var t=this.elements[e];if(typeof t.fn[this.mode]!="function")return null;var n=new jsButton(t.title,t.fn[this.mode],this,"jstb_"+e);if(t.icon!=undefined)n.icon=t.icon;return n},space:function(e){var t=new jsSpace(e);if(this.elements[e].width!==undefined)t.width=this.elements[e].width;return t},combo:function(e){var t=this.elements[e];var n=t[this.mode].list.length;if(typeof t[this.mode].fn!="function"||n==0){return null}else{var r={};for(var i=0;iAide";this.toolbar.appendChild(t);var n,r,i;for(var s in this.elements){n=this.elements[s];var o=n.type==undefined||n.type==""||n.disabled!=undefined&&n.disabled||n.context!=undefined&&n.context!=null&&n.context!=this.context;if(!o&&typeof this[n.type]=="function"){r=this[n.type](s);if(r)i=r.draw();if(i){this.toolNodes[s]=i;this.toolbar.appendChild(i)}}}},singleTag:function(e,t){e=e||null;t=t||e;if(!e||!t){return}this.encloseSelection(e,t)},encloseLineSelection:function(e,t,n){this.textarea.focus();e=e||"";t=t||"";var r,i,s,o,u,a;if(typeof document["selection"]!="undefined"){s=document.selection.createRange().text}else if(typeof this.textarea["setSelectionRange"]!="undefined"){r=this.textarea.selectionStart;i=this.textarea.selectionEnd;o=this.textarea.scrollTop;r=this.textarea.value.substring(0,r).replace(/[^\r\n]*$/g,"").length;i=this.textarea.value.length-this.textarea.value.substring(i,this.textarea.value.length).replace(/^[^\r\n]*/,"").length;s=this.textarea.value.substring(r,i)}if(s.match(/ $/)){s=s.substring(0,s.length-1);t=t+" "}if(typeof n=="function"){a=s?n.call(this,s):n("")}else{a=s?s:""}u=e+a+t;if(typeof document["selection"]!="undefined"){document.selection.createRange().text=u;var f=this.textarea.createTextRange();f.collapse(false);f.move("character",-t.length);f.select()}else if(typeof this.textarea["setSelectionRange"]!="undefined"){this.textarea.value=this.textarea.value.substring(0,r)+u+this.textarea.value.substring(i);if(s){this.textarea.setSelectionRange(r+u.length,r+u.length)}else{this.textarea.setSelectionRange(r+e.length,r+e.length)}this.textarea.scrollTop=o}},encloseSelection:function(e,t,n){this.textarea.focus();e=e||"";t=t||"";var r,i,s,o,u,a;if(typeof document["selection"]!="undefined"){s=document.selection.createRange().text}else if(typeof this.textarea["setSelectionRange"]!="undefined"){r=this.textarea.selectionStart;i=this.textarea.selectionEnd;o=this.textarea.scrollTop;s=this.textarea.value.substring(r,i)}if(s.match(/ $/)){s=s.substring(0,s.length-1);t=t+" "}if(typeof n=="function"){a=s?n.call(this,s):n("")}else{a=s?s:""}u=e+a+t;if(typeof document["selection"]!="undefined"){document.selection.createRange().text=u;var f=this.textarea.createTextRange();f.collapse(false);f.move("character",-t.length);f.select()}else if(typeof this.textarea["setSelectionRange"]!="undefined"){this.textarea.value=this.textarea.value.substring(0,r)+u+this.textarea.value.substring(i);if(s){this.textarea.setSelectionRange(r+u.length,r+u.length)}else{this.textarea.setSelectionRange(r+e.length,r+e.length)}this.textarea.scrollTop=o}},stripBaseURL:function(e){if(this.base_url!=""){var t=e.indexOf(this.base_url);if(t==0){e=e.substr(this.base_url.length)}}return e}};jsToolBar.prototype.resizeSetStartH=function(){this.dragStartH=this.textarea.offsetHeight+0};jsToolBar.prototype.resizeDragStart=function(e){var t=this;this.dragStartY=e.clientY;this.resizeSetStartH();document.addEventListener("mousemove",this.dragMoveHdlr=function(e){t.resizeDragMove(e)},false);document.addEventListener("mouseup",this.dragStopHdlr=function(e){t.resizeDragStop(e)},false)};jsToolBar.prototype.resizeDragMove=function(e){this.textarea.style.height=this.dragStartH+e.clientY-this.dragStartY+"px"};jsToolBar.prototype.resizeDragStop=function(e){document.removeEventListener("mousemove",this.dragMoveHdlr,false);document.removeEventListener("mouseup",this.dragStopHdlr,false)};jsToolBar.prototype.elements.strong={type:"button",title:"Strong",fn:{wiki:function(){this.singleTag("*")}}};jsToolBar.prototype.elements.em={type:"button",title:"Italic",fn:{wiki:function(){this.singleTag("_")}}};jsToolBar.prototype.elements.ins={type:"button",title:"Underline",fn:{wiki:function(){this.singleTag("+")}}};jsToolBar.prototype.elements.del={type:"button",title:"Deleted",fn:{wiki:function(){this.singleTag("-")}}};jsToolBar.prototype.elements.code={type:"button",title:"Code",fn:{wiki:function(){this.singleTag("@")}}};jsToolBar.prototype.elements.space1={type:"space"};jsToolBar.prototype.elements.h1={type:"button",title:"Heading 1",fn:{wiki:function(){this.encloseLineSelection("h1. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.h2={type:"button",title:"Heading 2",fn:{wiki:function(){this.encloseLineSelection("h2. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.h3={type:"button",title:"Heading 3",fn:{wiki:function(){this.encloseLineSelection("h3. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.space2={type:"space"};jsToolBar.prototype.elements.ul={type:"button",title:"Unordered list",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^)[#-]?\s*/g,"$1* ")})}}};jsToolBar.prototype.elements.ol={type:"button",title:"Ordered list",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^)[*-]?\s*/g,"$1# ")})}}};jsToolBar.prototype.elements.space3={type:"space"};jsToolBar.prototype.elements.bq={type:"button",title:"Quote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *([^\n]*)/g,"$1> $2")})}}};jsToolBar.prototype.elements.unbq={type:"button",title:"Unquote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2")})}}};jsToolBar.prototype.elements.pre={type:"button",title:"Preformatted text",fn:{wiki:function(){this.encloseLineSelection("
    \n","\n
    ")}}};jsToolBar.prototype.elements.space4={type:"space"};jsToolBar.prototype.elements.link={type:"button",title:"Wiki link",fn:{wiki:function(){this.encloseSelection("[[","]]")}}};jsToolBar.prototype.elements.img={type:"button",title:"Image",fn:{wiki:function(){this.encloseSelection("!","!")}}} \ No newline at end of file +function jsToolBar(e){if(!document.createElement){return}if(!e){return}if(typeof document["selection"]=="undefined"&&typeof e["setSelectionRange"]=="undefined"){return}this.textarea=e;this.editor=document.createElement("div");this.editor.className="jstEditor";this.textarea.parentNode.insertBefore(this.editor,this.textarea);this.editor.appendChild(this.textarea);this.toolbar=document.createElement("div");this.toolbar.className="jstElements";this.editor.parentNode.insertBefore(this.toolbar,this.editor);if(this.editor.addEventListener&&navigator.appVersion.match(/\bMSIE\b/)){this.handle=document.createElement("div");this.handle.className="jstHandle";var t=this.resizeDragStart;var n=this;this.handle.addEventListener("mousedown",function(e){t.call(n,e)},false);window.addEventListener("unload",function(){var e=n.handle.parentNode.removeChild(n.handle);delete n.handle},false);this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling)}this.context=null;this.toolNodes={}}function jsButton(e,t,n,r){if(typeof jsToolBar.strings=="undefined"){this.title=e||null}else{this.title=jsToolBar.strings[e]||e||null}this.fn=t||function(){};this.scope=n||null;this.className=r||null}function jsSpace(e){this.id=e||null;this.width=null}function jsCombo(e,t,n,r,i){this.title=e||null;this.options=t||null;this.scope=n||null;this.fn=r||function(){};this.className=i||null}jsButton.prototype.draw=function(){if(!this.scope)return null;var e=document.createElement("button");e.setAttribute("type","button");e.tabIndex=200;if(this.className)e.className=this.className;e.title=this.title;var t=document.createElement("span");t.appendChild(document.createTextNode(this.title));e.appendChild(t);if(this.icon!=undefined){e.style.backgroundImage="url("+this.icon+")"}if(typeof this.fn=="function"){var n=this;e.onclick=function(){try{n.fn.apply(n.scope,arguments)}catch(e){}return false}}return e};jsSpace.prototype.draw=function(){var e=document.createElement("span");if(this.id)e.id=this.id;e.appendChild(document.createTextNode(String.fromCharCode(160)));e.className="jstSpacer";if(this.width)e.style.marginRight=this.width+"px";return e};jsCombo.prototype.draw=function(){if(!this.scope||!this.options)return null;var e=document.createElement("select");if(this.className)e.className=className;e.title=this.title;for(var t in this.options){var n=document.createElement("option");n.value=t;n.appendChild(document.createTextNode(this.options[t]));e.appendChild(n)}var r=this;e.onchange=function(){try{r.fn.call(r.scope,this.value)}catch(e){alert(e)}return false};return e};jsToolBar.prototype={base_url:"",mode:"wiki",elements:{},help_link:"",getMode:function(){return this.mode},setMode:function(e){this.mode=e||"wiki"},switchMode:function(e){e=e||"wiki";this.draw(e)},setHelpLink:function(e){this.help_link=e},button:function(e){var t=this.elements[e];if(typeof t.fn[this.mode]!="function")return null;var n=new jsButton(t.title,t.fn[this.mode],this,"jstb_"+e);if(t.icon!=undefined)n.icon=t.icon;return n},space:function(e){var t=new jsSpace(e);if(this.elements[e].width!==undefined)t.width=this.elements[e].width;return t},combo:function(e){var t=this.elements[e];var n=t[this.mode].list.length;if(typeof t[this.mode].fn!="function"||n==0){return null}else{var r={};for(var i=0;i $2")})}}};jsToolBar.prototype.elements.unbq={type:"button",title:"Unquote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2")})}}};jsToolBar.prototype.elements.pre={type:"button",title:"Preformatted text",fn:{wiki:function(){this.encloseLineSelection("
    \n","\n
    ")}}};jsToolBar.prototype.elements.space4={type:"space"};jsToolBar.prototype.elements.link={type:"button",title:"Wiki link",fn:{wiki:function(){this.encloseSelection("[[","]]")}}};jsToolBar.prototype.elements.img={type:"button",title:"Image",fn:{wiki:function(){this.encloseSelection("!","!")}}};jsToolBar.prototype.elements.space5={type:"space"};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 038ba2d95de8 -r 261b3d9a4903 public/javascripts/jstoolbar/jstoolbar.js --- a/public/javascripts/jstoolbar/jstoolbar.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/jstoolbar/jstoolbar.js Tue Jan 14 14:37:42 2014 +0000 @@ -207,12 +207,6 @@ } this.toolNodes = {}; // vide les raccourcis DOM/**/ - var h = document.createElement('div'); - h.className = 'help' - h.innerHTML = this.help_link; - 'Aide'; - this.toolbar.appendChild(h); - // Draw toolbar elements var b, tool, newTool; diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/jstoolbar/lang/jstoolbar-az.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/public/javascripts/jstoolbar/lang/jstoolbar-az.js Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Italic'] = 'Italic'; +jsToolBar.strings['Underline'] = 'Underline'; +jsToolBar.strings['Deleted'] = 'Deleted'; +jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Heading 1'] = 'Heading 1'; +jsToolBar.strings['Heading 2'] = 'Heading 2'; +jsToolBar.strings['Heading 3'] = 'Heading 3'; +jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/jstoolbar/lang/jstoolbar-lt.js --- a/public/javascripts/jstoolbar/lang/jstoolbar-lt.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/jstoolbar/lang/jstoolbar-lt.js Tue Jan 14 14:37:42 2014 +0000 @@ -9,8 +9,8 @@ jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Nenumeruotas sÄ…raÅ¡as'; jsToolBar.strings['Ordered list'] = 'Numeruotas sÄ…raÅ¡as'; -jsToolBar.strings['Quote'] = 'Quote'; -jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Quote'] = 'Cituoti'; +jsToolBar.strings['Unquote'] = 'PaÅ¡alinti citavimÄ…'; jsToolBar.strings['Preformatted text'] = 'Preformatuotas tekstas'; jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį'; jsToolBar.strings['Image'] = 'Paveikslas'; diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/jstoolbar/lang/jstoolbar-sk.js --- a/public/javascripts/jstoolbar/lang/jstoolbar-sk.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/jstoolbar/lang/jstoolbar-sk.js Tue Jan 14 14:37:42 2014 +0000 @@ -4,13 +4,13 @@ jsToolBar.strings['Underline'] = 'PodÄiarknuté'; jsToolBar.strings['Deleted'] = 'PreÅ¡krtnuté'; jsToolBar.strings['Code'] = 'Zobrazenie kódu'; -jsToolBar.strings['Heading 1'] = 'Záhlavie 1'; -jsToolBar.strings['Heading 2'] = 'Záhlavie 2'; -jsToolBar.strings['Heading 3'] = 'Záhlavie 3'; -jsToolBar.strings['Unordered list'] = 'Zoznam'; -jsToolBar.strings['Ordered list'] = 'Zoradený zoznam'; -jsToolBar.strings['Quote'] = 'Citácia'; -jsToolBar.strings['Unquote'] = 'Odstránenie citácie'; +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'] = 'Link na Wiki stránku'; +jsToolBar.strings['Wiki link'] = 'Odkaz na wikistránku'; jsToolBar.strings['Image'] = 'Obrázok'; diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/jstoolbar/textile.js --- a/public/javascripts/jstoolbar/textile.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/jstoolbar/textile.js Tue Jan 14 14:37:42 2014 +0000 @@ -198,3 +198,14 @@ 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 038ba2d95de8 -r 261b3d9a4903 public/javascripts/project_identifier.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/public/javascripts/project_identifier.js Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,78 @@ +// Automatic project identifier generation + +function generateProjectIdentifier(identifier, maxlength) { + var diacriticsMap = [ + {'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g}, + {'base':'aa','letters':/[\uA733\uA732]/g}, + {'base':'ae','letters':/[\u00E4\u00E6\u01FD\u01E3\u00C4\u00C6\u01FC\u01E2]/g}, + {'base':'ao','letters':/[\uA735\uA734]/g}, + {'base':'au','letters':/[\uA737\uA736]/g}, + {'base':'av','letters':/[\uA739\uA73B\uA738\uA73A]/g}, + {'base':'ay','letters':/[\uA73D\uA73C]/g}, + {'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g}, + {'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g}, + {'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g}, + {'base':'dz','letters':/[\u01F3\u01C6\u01F1\u01C4\u01F2\u01C5]/g}, + {'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g}, + {'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g}, + {'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g}, + {'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g}, + {'base':'hv','letters':/[\u0195]/g}, + {'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g}, + {'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249\u004A\u24BF\uFF2A\u0134\u0248]/g}, + {'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g}, + {'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g}, + {'base':'lj','letters':/[\u01C9\u01C7\u01C8]/g}, + {'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g}, + {'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g}, + {'base':'nj','letters':/[\u01CC\u01CA\u01CB]/g}, + {'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g}, + {'base':'oe','letters': /[\u00F6\u0153\u00D6\u0152]/g}, + {'base':'oi','letters':/[\u01A3\u01A2]/g}, + {'base':'ou','letters':/[\u0223\u0222]/g}, + {'base':'oo','letters':/[\uA74F\uA74E]/g}, + {'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g}, + {'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759\u0051\u24C6\uFF31\uA756\uA758\u024A]/g}, + {'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g}, + {'base':'s','letters':/[\u0073\u24E2\uFF53\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g}, + {'base':'ss','letters':/[\u00DF]/g}, + {'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g}, + {'base':'tz','letters':/[\uA729\uA728]/g}, + {'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g}, + {'base':'ue','letters':/[\u00FC\u00DC]/g}, + {'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g}, + {'base':'vy','letters':/[\uA761\uA760]/g}, + {'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g}, + {'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D\u0058\u24CD\uFF38\u1E8A\u1E8C]/g}, + {'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g}, + {'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g} + ]; + + for(var i=0; i hyphen + identifier = identifier.replace(/^[-_\d]*|[-_]*$/g, ''); // remove hyphens/underscores and numbers at beginning and hyphens/underscores at end + identifier = identifier.toLowerCase(); // to lower + identifier = identifier.substr(0, maxlength); // max characters + return identifier; +} + +function autoFillProjectIdentifier() { + var locked = ($('#project_identifier').val() != ''); + var maxlength = parseInt($('#project_identifier').attr('maxlength')); + + $('#project_name').keyup(function(){ + if(!locked) { + $('#project_identifier').val(generateProjectIdentifier($('#project_name').val(), maxlength)); + } + }); + + $('#project_identifier').keyup(function(){ + locked = ($('#project_identifier').val() != '' && $('#project_identifier').val() != generateProjectIdentifier($('#project_name').val(), maxlength)); + }); +} + +$(document).ready(function(){ + autoFillProjectIdentifier(); +}); diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/revision_graph.js --- a/public/javascripts/revision_graph.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/revision_graph.js Tue Jan 14 14:37:42 2014 +0000 @@ -43,7 +43,7 @@ revisionGraph.circle(x, y, 3) .attr({ fill: colors[commit.space], - stroke: 'none', + stroke: 'none' }).toFront(); // paths to parents $.each(commit.parent_scmids, function(index, parent_scmid) { diff -r 038ba2d95de8 -r 261b3d9a4903 public/javascripts/select_list_move.js --- a/public/javascripts/select_list_move.js Fri Jun 14 09:05:06 2013 +0100 +++ b/public/javascripts/select_list_move.js Tue Jan 14 14:37:42 2014 +0000 @@ -1,14 +1,12 @@ var NS4 = (navigator.appName == "Netscape" && parseInt(navigator.appVersion) < 5); -function addOption(theSel, theText, theValue) -{ +function addOption(theSel, theText, theValue) { var newOpt = new Option(theText, theValue); var selLength = theSel.length; theSel.options[selLength] = newOpt; } -function swapOptions(theSel, index1, index2) -{ +function swapOptions(theSel, index1, index2) { var text, value; text = theSel.options[index1].text; value = theSel.options[index1].value; @@ -18,42 +16,31 @@ theSel.options[index2].value = value; } -function deleteOption(theSel, theIndex) -{ +function deleteOption(theSel, theIndex) { var selLength = theSel.length; - if(selLength>0) - { + if (selLength > 0) { theSel.options[theIndex] = null; } } -function moveOptions(theSelFrom, theSelTo) -{ - +function moveOptions(theSelFrom, theSelTo) { var selLength = theSelFrom.length; var selectedText = new Array(); var selectedValues = new Array(); var selectedCount = 0; - var i; - - for(i=selLength-1; i>=0; i--) - { - if(theSelFrom.options[i].selected) - { + for (i = selLength - 1; i >= 0; i--) { + if (theSelFrom.options[i].selected) { selectedText[selectedCount] = theSelFrom.options[i].text; selectedValues[selectedCount] = theSelFrom.options[i].value; deleteOption(theSelFrom, i); selectedCount++; } } - - for(i=selectedCount-1; i>=0; i--) - { + for (i = selectedCount - 1; i >= 0; i--) { addOption(theSelTo, selectedText[i], selectedValues[i]); } - - if(NS4) history.go(0); + if (NS4) history.go(0); } function moveOptionUp(theSel) { @@ -73,11 +60,7 @@ } // OK -function selectAllOptions(id) -{ - var select = $('#'+id);/* - for (var i=0; ilegend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } +fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); } fieldset#date-range p { margin: 2px 0 2px 0; } fieldset#filters table { border-collapse: collapse; } @@ -372,6 +376,8 @@ div#activity dd .description, #search-results dd .description { font-style: italic; } div#activity span.project:after, #search-results span.project:after { content: " -"; } div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } +div#activity dt.grouped {margin-left:5em;} +div#activity dd.grouped {margin-left:9em;} #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } @@ -431,7 +437,7 @@ #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; } .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } -#notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;} +#notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;} #related-issues li img {vertical-align:middle;} @@ -452,10 +458,11 @@ table.fields_permissions td.required {background:#d88;} textarea#custom_field_possible_values {width: 99%} +textarea#custom_field_default_value {width: 99%} + input#content_comments {width: 99%} -.pagination {font-size: 90%} -p.pagination {margin-top:8px;} +p.pagination {margin-top:8px; font-size: 90%} /***** Tabular forms ******/ .tabular p{ @@ -525,9 +532,16 @@ span.required {color: #bb0000;} .summary {font-style: italic;} -#attachments_fields input.description {margin-left: 8px; width:340px;} +#attachments_fields input.description {margin-left:4px; width:340px;} #attachments_fields span {display:block; white-space:nowrap;} -#attachments_fields img {vertical-align: middle;} +#attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;} +#attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;} +#attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;} +#attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; } +a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;} +a.remove-upload:hover {text-decoration:none !important;} + +div.fileover { background-color: lavender; } div.attachments { margin-top: 12px; } div.attachments p { margin:4px 0 2px 0; } @@ -548,29 +562,33 @@ textarea.text_cf {width:90%;} -/* Project members tab */ -div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% } -div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% } -div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; } -div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; } -div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; } -div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; } +#tab-content-modules fieldset p {margin:3px 0 4px 0;} + +#tab-content-members .splitcontentleft, #tab-content-memberships .splitcontentleft, #tab-content-users .splitcontentleft {width: 64%;} +#tab-content-members .splitcontentright, #tab-content-memberships .splitcontentright, #tab-content-users .splitcontentright {width: 34%;} +#tab-content-members fieldset, #tab-content-memberships fieldset, #tab-content-users fieldset {padding:1em; margin-bottom: 1em;} +#tab-content-members fieldset legend, #tab-content-memberships fieldset legend, #tab-content-users fieldset legend {font-weight: bold;} +#tab-content-members fieldset label, #tab-content-memberships fieldset label, #tab-content-users fieldset label {display: block;} +#tab-content-members #principals, #tab-content-users #principals {max-height: 400px; overflow: auto;} + +#tab-content-memberships .splitcontentright select {width:90%} #users_for_watcher {height: 200px; overflow:auto;} #users_for_watcher label {display: block;} table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } -input#principal_search, input#user_search {width:100%} -input#principal_search, input#user_search { - background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px; - border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%; +input#principal_search, input#user_search {width:90%} + +input.autocomplete { + background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important; + border:1px solid #9EB1C2; border-radius:2px; height:1.5em; } -input#principal_search.ajax-loading, input#user_search.ajax-loading { +input.autocomplete.ajax-loading { background-image: url(../images/loading.gif); } -* html div#tab-content-members fieldset div { height: 450px; } +.role-visibility {padding-left:2em;} /***** Flash & error messages ****/ #errorExplanation, div.flash, .nodata, .warning, .conflict { @@ -693,7 +711,7 @@ table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } table.progress td.done { background: #D3EDD3 none repeat scroll 0%; } table.progress td.todo { background: #eee none repeat scroll 0%; } -p.pourcent {font-size: 80%;} +p.percent {font-size: 80%;} p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;} #roadmap table.progress td { height: 1.2em; } @@ -1025,6 +1043,10 @@ .hascontextmenu { cursor: context-menu; } +/* Custom JQuery styles */ +.ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;} + + /************* CodeRay styles *************/ .syntaxhl div {display: inline;} .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;} @@ -1100,13 +1122,13 @@ .syntaxhl .type { color:#339; font-weight:bold } .syntaxhl .value { color: #088; } .syntaxhl .variable { color:#037 } - + .syntaxhl .insert { background: hsla(120,100%,50%,0.12) } .syntaxhl .delete { background: hsla(0,100%,50%,0.12) } .syntaxhl .change { color: #bbf; background: #007; } .syntaxhl .head { color: #f8f; background: #505 } .syntaxhl .head .filename { color: white; } - + .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; } .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; } diff -r 038ba2d95de8 -r 261b3d9a4903 public/stylesheets/jquery/images/ui-bg_gloss-wave_35_759fcf_500x100.png Binary file public/stylesheets/jquery/images/ui-bg_gloss-wave_35_759fcf_500x100.png has changed diff -r 038ba2d95de8 -r 261b3d9a4903 public/stylesheets/jquery/jquery-ui-1.8.21.css --- a/public/stylesheets/jquery/jquery-ui-1.8.21.css Fri Jun 14 09:05:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,563 +0,0 @@ -/*! - * jQuery UI CSS Framework 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Theming/API - */ - -/* Layout helpers -----------------------------------*/ -.ui-helper-hidden { display: none; } -.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,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); } - - -/* Interaction Cues -----------------------------------*/ -.ui-state-disabled { cursor: default !important; } - - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } - - -/* Misc visuals -----------------------------------*/ - -/* Overlays */ -.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } - - -/*! - * jQuery UI CSS Framework 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Theming/API - * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,%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 - */ - - -/* Component containers -----------------------------------*/ -.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 #dddddd; background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x; color: #333333; } -.ui-widget-content a { color: #333333; } -.ui-widget-header { border: 1px solid #628db6; background: #759fcf url(images/ui-bg_gloss-wave_35_759fcf_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; } -.ui-widget-header a { color: #ffffff; } - -/* Interaction states -----------------------------------*/ -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; 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 { 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: #ffffff 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-widget :active { outline: none; } - -/* Interaction Cues -----------------------------------*/ -.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: #ffffff; } -.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; } -.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; } -.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; } - -/* Icons -----------------------------------*/ - -/* states and images */ -.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); } - -/* positioning */ -.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-off { background-position: -96px -144px; } -.ui-icon-radio-on { 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 is deprecated, use ui-icon-seek-start instead */ -.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; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.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; } - -/* Overlays */ -.ui-widget-overlay { background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat; opacity: .50;filter:Alpha(Opacity=50); } -.ui-widget-shadow { margin: -5px 0 0 -5px; padding: 5px; background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 5px; -khtml-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }/*! - * jQuery UI Resizable 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Resizable#theming - */ -.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;}/*! - * jQuery UI Selectable 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Selectable#theming - */ -.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } -/*! - * jQuery UI Accordion 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Accordion#theming - */ -/* IE/Win - Fix animation bug - #4615 */ -.ui-accordion { width: 100%; } -.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } -.ui-accordion .ui-accordion-li-fix { display: inline; } -.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } -.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } -.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } -.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } -.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } -.ui-accordion .ui-accordion-content-active { display: block; } -/*! - * jQuery UI Autocomplete 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Autocomplete#theming - */ -.ui-autocomplete { position: absolute; cursor: default; } - -/* workarounds */ -* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ - -/* - * jQuery UI Menu 1.8.22 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Menu#theming - */ -.ui-menu { - list-style:none; - padding: 2px; - margin: 0; - display:block; - float: left; -} -.ui-menu .ui-menu { - margin-top: -3px; -} -.ui-menu .ui-menu-item { - margin:0; - padding: 0; - zoom: 1; - float: left; - clear: left; - width: 100%; -} -.ui-menu .ui-menu-item a { - text-decoration:none; - display:block; - padding:.2em .4em; - line-height:1.5; - zoom:1; -} -.ui-menu .ui-menu-item a.ui-state-hover, -.ui-menu .ui-menu-item a.ui-state-active { - font-weight: normal; - margin: -1px; -} -/*! - * jQuery UI Button 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Button#theming - */ -.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ -.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ -button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ -.ui-button-icons-only { width: 3.4em; } -button.ui-button-icons-only { width: 3.7em; } - -/*button text element */ -.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; } -/* no icon support for input elements, provide padding by default */ -input.ui-button { padding: .4em 1em; } - -/*button icon element(s) */ -.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; } - -/*button sets*/ -.ui-buttonset { margin-right: 7px; } -.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } - -/* workarounds */ -button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ -/*! - * jQuery UI Dialog 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Dialog#theming - */ -.ui-dialog { position: absolute; 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; } -/*! - * jQuery UI Slider 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Slider#theming - */ -.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; }/*! - * jQuery UI Tabs 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Tabs#theming - */ -.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ -.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: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; 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-selected { margin-bottom: 0; padding-bottom: 1px; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } -.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ -.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } -.ui-tabs .ui-tabs-hide { display: none !important; } -/*! - * jQuery UI Datepicker 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Datepicker#theming - */ -.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; } - -/* with multiple calendars */ -.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; } - -/* RTL support */ -.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; } - -/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ -.ui-datepicker-cover { - position: absolute; /*must have*/ - z-index: -1; /*must have*/ - filter: mask(); /*must have*/ - top: -4px; /*must have*/ - left: -4px; /*must have*/ - width: 200px; /*must have*/ - height: 200px; /*must have*/ -}/*! - * jQuery UI Progressbar 1.8.22 - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar#theming - */ -.ui-progressbar { height:2em; text-align: left; overflow: hidden; } -.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } diff -r 038ba2d95de8 -r 261b3d9a4903 public/stylesheets/jquery/jquery-ui-1.9.2.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/public/stylesheets/jquery/jquery-ui-1.9.2.css Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 public/stylesheets/jstoolbar.css --- a/public/stylesheets/jstoolbar.css Fri Jun 14 09:05:06 2013 +0100 +++ b/public/stylesheets/jstoolbar.css Tue Jan 14 14:37:42 2014 +0000 @@ -95,3 +95,6 @@ .jstb_img { background-image: url(../images/jstoolbar/bt_img.png); } +.jstb_help { + background-image: url(../images/help.png); +} diff -r 038ba2d95de8 -r 261b3d9a4903 public/stylesheets/scm.css --- a/public/stylesheets/scm.css Fri Jun 14 09:05:06 2013 +0100 +++ b/public/stylesheets/scm.css Tue Jan 14 14:37:42 2014 +0000 @@ -64,6 +64,9 @@ font-family:"Liberation Mono", Courier, monospace; font-size:12px; } +table.filecontent tr:target th.line-num { background-color:#E0E0E0; color: #777; } +table.filecontent tr:target td.line-code { background-color:#DDEEFF; } + /* 12 different colors for the annonate view */ table.annotate tr.bloc-0 {background: #FFFFBF;} table.annotate tr.bloc-1 {background: #EABFFF;} diff -r 038ba2d95de8 -r 261b3d9a4903 test/extra/redmine_pm/repository_git_test.rb --- a/test/extra/redmine_pm/repository_git_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/extra/redmine_pm/repository_git_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/extra/redmine_pm/repository_subversion_test.rb --- a/test/extra/redmine_pm/repository_subversion_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/extra/redmine_pm/repository_subversion_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/extra/redmine_pm/test_case.rb --- a/test/extra/redmine_pm/test_case.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/extra/redmine_pm/test_case.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/attachments.yml --- a/test/fixtures/attachments.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/attachments.yml Tue Jan 14 14:37:42 2014 +0000 @@ -4,6 +4,7 @@ downloads: 0 content_type: text/plain disk_filename: 060719210727_error281.txt + disk_directory: "2006/07" container_id: 3 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 1 @@ -16,6 +17,7 @@ downloads: 0 content_type: text/plain disk_filename: 060719210727_document.txt + disk_directory: "2006/07" container_id: 1 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 2 @@ -28,6 +30,7 @@ downloads: 0 content_type: image/gif disk_filename: 060719210727_logo.gif + disk_directory: "2006/07" container_id: 4 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 3 @@ -42,6 +45,7 @@ container_id: 3 downloads: 0 disk_filename: 060719210727_source.rb + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 4 filesize: 153 @@ -55,6 +59,7 @@ container_id: 3 downloads: 0 disk_filename: 060719210727_changeset_iso8859-1.diff + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 5 filesize: 687 @@ -67,6 +72,7 @@ container_id: 3 downloads: 0 disk_filename: 060719210727_archive.zip + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 6 filesize: 157 @@ -79,6 +85,7 @@ container_id: 4 downloads: 0 disk_filename: 060719210727_archive.zip + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 7 filesize: 157 @@ -91,6 +98,7 @@ container_id: 1 downloads: 0 disk_filename: 060719210727_project_file.zip + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 8 filesize: 320 @@ -103,6 +111,7 @@ container_id: 1 downloads: 0 disk_filename: 060719210727_archive.zip + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 9 filesize: 452 @@ -115,6 +124,7 @@ container_id: 2 downloads: 0 disk_filename: 060719210727_picture.jpg + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 10 filesize: 452 @@ -127,6 +137,7 @@ container_id: 1 downloads: 0 disk_filename: 060719210727_picture.jpg + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 11 filesize: 452 @@ -139,6 +150,7 @@ container_id: 1 downloads: 0 disk_filename: 060719210727_version_file.zip + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 12 filesize: 452 @@ -151,6 +163,7 @@ container_id: 1 downloads: 0 disk_filename: 060719210727_foo.zip + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 13 filesize: 452 @@ -163,6 +176,7 @@ container_id: 3 downloads: 0 disk_filename: 060719210727_changeset_utf8.diff + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 id: 14 filesize: 687 @@ -176,6 +190,7 @@ container_id: 14 downloads: 0 disk_filename: 060719210727_changeset_utf8.diff + disk_directory: "2006/07" digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 filesize: 687 filename: private.diff @@ -187,6 +202,7 @@ downloads: 0 created_on: 2010-11-23 16:14:50 +09:00 disk_filename: 101123161450_testfile_1.png + disk_directory: "2010/11" container_id: 14 digest: 8e0294de2441577c529f170b6fb8f638 id: 16 @@ -200,6 +216,7 @@ downloads: 0 created_on: 2010-12-23 16:14:50 +09:00 disk_filename: 101223161450_testfile_2.png + disk_directory: "2010/12" container_id: 14 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca id: 17 @@ -213,6 +230,7 @@ downloads: 0 created_on: 2011-01-23 16:14:50 +09:00 disk_filename: 101123161450_testfile_1.png + disk_directory: "2010/11" container_id: 14 digest: 8e0294de2441577c529f170b6fb8f638 id: 18 @@ -226,6 +244,7 @@ downloads: 0 created_on: 2011-02-23 16:14:50 +09:00 disk_filename: 101223161450_testfile_2.png + disk_directory: "2010/12" container_id: 14 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca id: 19 @@ -234,3 +253,17 @@ filename: Testテスト.PNG filesize: 3582 author_id: 2 +attachments_020: + content_type: text/plain + downloads: 0 + created_on: 2012-05-12 16:14:50 +09:00 + disk_filename: 120512161450_root_attachment.txt + disk_directory: + container_id: 14 + digest: b0fe2abdb2599743d554a61d7da7ff74 + id: 20 + container_type: Issue + description: "" + filename: root_attachment.txt + filesize: 54 + author_id: 2 diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/changesets.yml --- a/test/fixtures/changesets.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/changesets.yml Tue Jan 14 14:37:42 2014 +0000 @@ -3,8 +3,9 @@ commit_date: 2007-04-11 committed_on: 2007-04-11 15:14:44 +02:00 revision: 1 + scmid: 691322a8eb01e11fd7 id: 100 - comments: My very first commit + comments: 'My very first commit do not escaping #<>&' repository_id: 10 committer: dlopper user_id: 3 diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/custom_fields_trackers.yml --- a/test/fixtures/custom_fields_trackers.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/custom_fields_trackers.yml Tue Jan 14 14:37:42 2014 +0000 @@ -17,3 +17,12 @@ custom_fields_trackers_006: custom_field_id: 6 tracker_id: 3 +custom_fields_trackers_007: + custom_field_id: 8 + tracker_id: 1 +custom_fields_trackers_008: + custom_field_id: 8 + tracker_id: 2 +custom_fields_trackers_009: + custom_field_id: 8 + tracker_id: 3 diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-12641-ja.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-12641-ja.diff Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,25 @@ +# HG changeset patch +# User tmaruyama +# Date 1362559296 0 +# Node ID ee54942e0289c30bea1b1973750b698b1ee7c466 +# Parent 738777832f379f6f099c25251593fc57bc17f586 +fix some Japanese "issue" translations (#13350) + +Contributed by Go MAEDA. + +diff --git a/config/locales/ja.yml b/config/locales/ja.yml +--- a/config/locales/ja.yml ++++ b/config/locales/ja.yml +@@ -904,9 +904,9 @@ ja: + 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_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}文字ã¾ã§ã§ã™ã€‚" diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-12641-ru.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-12641-ru.diff Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,19 @@ +# HG changeset patch +# User tmaruyama +# Date 1355872765 0 +# Node ID 8a13ebed1779c2e85fa644ecdd0de81996c969c4 +# Parent 5c3c5f917ae92f278fe42c6978366996595b0796 +Russian "about_x_hours" translation changed by Mikhail Velkin (#12640) + +diff --git a/config/locales/ru.yml b/config/locales/ru.yml +--- a/config/locales/ru.yml ++++ b/config/locales/ru.yml +@@ -115,7 +115,7 @@ ru: + one: "около %{count} чаÑа" + few: "около %{count} чаÑов" + many: "около %{count} чаÑов" +- other: "около %{count} чаÑа" ++ other: "около %{count} чаÑов" + x_hours: + one: "1 чаÑ" + other: "%{count} чаÑов" diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-13644-1.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-13644-1.diff Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-13644-2.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-13644-2.diff Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-13644-3.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-13644-3.diff Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,7 @@ +--- a.txt 2013-07-27 06:03:49.133257759 +0900 ++++ b.txt 2013-07-27 06:03:58.791221118 +0900 +@@ -1,3 +1,3 @@ + aaaa +-日本記 ++日本娘 + bbbb diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-13644-4.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-13644-4.diff Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,7 @@ +--- a.txt 2013-07-27 04:20:45.973229414 +0900 ++++ b.txt 2013-07-27 04:20:52.366228105 +0900 +@@ -1,3 +1,3 @@ + aaaa +-日本記 ++日本誘 + bbbb diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/diffs/issue-13644-5.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/diffs/issue-13644-5.diff Tue Jan 14 14:37:42 2014 +0000 @@ -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 038ba2d95de8 -r 261b3d9a4903 test/fixtures/files/060719210727_archive.zip Binary file test/fixtures/files/060719210727_archive.zip has changed diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/files/060719210727_changeset_iso8859-1.diff --- a/test/fixtures/files/060719210727_changeset_iso8859-1.diff Fri Jun 14 09:05:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -Index: trunk/app/controllers/issues_controller.rb -=================================================================== ---- trunk/app/controllers/issues_controller.rb (révision 1483) -+++ trunk/app/controllers/issues_controller.rb (révision 1484) -@@ -149,7 +149,7 @@ - attach_files(@issue, params[:attachments]) - flash[:notice] = 'Demande créée avec succès' - Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') -- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project -+ redirect_to :controller => 'issues', :action => 'show', :id => @issue - return - end - end diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/files/060719210727_changeset_utf8.diff --- a/test/fixtures/files/060719210727_changeset_utf8.diff Fri Jun 14 09:05:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -Index: trunk/app/controllers/issues_controller.rb -=================================================================== ---- trunk/app/controllers/issues_controller.rb (révision 1483) -+++ trunk/app/controllers/issues_controller.rb (révision 1484) -@@ -149,7 +149,7 @@ - attach_files(@issue, params[:attachments]) - flash[:notice] = 'Demande créée avec succès' - Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') -- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project -+ redirect_to :controller => 'issues', :action => 'show', :id => @issue - return - end - end diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/files/060719210727_source.rb --- a/test/fixtures/files/060719210727_source.rb Fri Jun 14 09:05:06 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -# The Greeter class -class Greeter - def initialize(name) - @name = name.capitalize - end - - def salute - puts "Hello #{@name}!" - end -end diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/files/101123161450_testfile_1.png Binary file test/fixtures/files/101123161450_testfile_1.png has changed diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/files/101223161450_testfile_2.png Binary file test/fixtures/files/101223161450_testfile_2.png has changed diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/issues.yml --- a/test/fixtures/issues.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/issues.yml Tue Jan 14 14:37:42 2014 +0000 @@ -152,6 +152,7 @@ root_id: 8 lft: 1 rgt: 2 + closed_on: <%= 3.days.ago.to_s(:db) %> issues_009: created_on: <%= 1.minute.ago.to_s(:db) %> project_id: 5 @@ -209,6 +210,7 @@ root_id: 11 lft: 1 rgt: 2 + closed_on: <%= 1.day.ago.to_s(:db) %> issues_012: created_on: <%= 3.days.ago.to_s(:db) %> project_id: 1 @@ -228,6 +230,7 @@ root_id: 12 lft: 1 rgt: 2 + closed_on: <%= 1.day.ago.to_s(:db) %> issues_013: created_on: <%= 5.days.ago.to_s(:db) %> project_id: 3 diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/journal_details.yml --- a/test/fixtures/journal_details.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/journal_details.yml Tue Jan 14 14:37:42 2014 +0000 @@ -14,7 +14,7 @@ prop_key: done_ratio journal_id: 1 journal_details_003: - old_value: nil + old_value: property: attr id: 3 value: "6" @@ -35,7 +35,7 @@ prop_key: 2 journal_id: 3 journal_details_006: - old_value: nil + old_value: property: attachment id: 6 value: 060719210727_picture.jpg diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/mail_handler/multiple_text_parts.eml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/fixtures/mail_handler/multiple_text_parts.eml Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,62 @@ +From JSmith@somenet.foo Fri Mar 22 08:30:28 2013 +From: John Smith +Content-Type: multipart/mixed; boundary="Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9" +Message-Id: +Mime-Version: 1.0 (Mac OS X Mail 6.3 \(1503\)) +Subject: Test with multiple text parts +Date: Fri, 22 Mar 2013 17:30:20 +0200 +To: redmine@somenet.foo +X-Mailer: Apple Mail (2.1503) + + + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=us-ascii + +The first text part. + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9 +Content-Disposition: inline; + filename=1st.pdf +Content-Type: application/pdf; + x-unix-mode=0644; + name="1st.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9G + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=us-ascii + +The second text part. + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9 +Content-Disposition: inline; + filename=2nd.pdf +Content-Type: application/pdf; + x-unix-mode=0644; + name="2nd.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9G + + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=us-ascii + +The third one. + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=us-ascii +Content-Disposition: attachment; filename="textfile.txt" + +Plain text attachment + +--Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9-- diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/queries.yml --- a/test/fixtures/queries.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/queries.yml Tue Jan 14 14:37:42 2014 +0000 @@ -1,8 +1,9 @@ --- queries_001: id: 1 + type: IssueQuery project_id: 1 - is_public: true + visibility: 2 name: Multiple custom fields query filters: | --- @@ -23,8 +24,9 @@ column_names: queries_002: id: 2 + type: IssueQuery project_id: 1 - is_public: false + visibility: 0 name: Private query for cookbook filters: | --- @@ -41,8 +43,9 @@ column_names: queries_003: id: 3 + type: IssueQuery project_id: - is_public: false + visibility: 0 name: Private query for all projects filters: | --- @@ -55,8 +58,9 @@ column_names: queries_004: id: 4 + type: IssueQuery project_id: - is_public: true + visibility: 2 name: Public query for all projects filters: | --- @@ -69,8 +73,9 @@ column_names: queries_005: id: 5 + type: IssueQuery project_id: - is_public: true + visibility: 2 name: Open issues by priority and tracker filters: | --- @@ -89,8 +94,9 @@ - asc queries_006: id: 6 + type: IssueQuery project_id: - is_public: true + visibility: 2 name: Open issues grouped by tracker filters: | --- @@ -108,8 +114,9 @@ - desc queries_007: id: 7 + type: IssueQuery project_id: 2 - is_public: true + visibility: 2 name: Public query for project 2 filters: | --- @@ -122,8 +129,9 @@ column_names: queries_008: id: 8 + type: IssueQuery project_id: 2 - is_public: false + visibility: 0 name: Private query for project 2 filters: | --- @@ -136,8 +144,9 @@ column_names: queries_009: id: 9 + type: IssueQuery project_id: - is_public: true + visibility: 2 name: Open issues grouped by list custom field filters: | --- diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/repositories.yml --- a/test/fixtures/repositories.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/repositories.yml Tue Jan 14 14:37:42 2014 +0000 @@ -8,6 +8,7 @@ login: "" type: Repository::Subversion is_default: true + created_on: 2006-07-19 19:04:21 +02:00 repositories_002: project_id: 2 url: svn://localhost/test @@ -17,3 +18,4 @@ login: "" type: Repository::Subversion is_default: true + created_on: 2006-07-19 19:04:21 +02:00 diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/repositories/bazaar_repository.tar.gz Binary file test/fixtures/repositories/bazaar_repository.tar.gz has changed diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/roles.yml --- a/test/fixtures/roles.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/roles.yml Tue Jan 14 14:37:42 2014 +0000 @@ -38,7 +38,9 @@ - :manage_news - :comment_news - :view_documents - - :manage_documents + - :add_documents + - :edit_documents + - :delete_documents - :view_wiki_pages - :export_wiki_pages - :view_wiki_edits @@ -89,7 +91,9 @@ - :manage_news - :comment_news - :view_documents - - :manage_documents + - :add_documents + - :edit_documents + - :delete_documents - :view_wiki_pages - :view_wiki_edits - :edit_wiki_pages @@ -131,7 +135,9 @@ - :manage_news - :comment_news - :view_documents - - :manage_documents + - :add_documents + - :edit_documents + - :delete_documents - :view_wiki_pages - :view_wiki_edits - :edit_wiki_pages @@ -163,7 +169,6 @@ - :view_time_entries - :comment_news - :view_documents - - :manage_documents - :view_wiki_pages - :view_wiki_edits - :edit_wiki_pages diff -r 038ba2d95de8 -r 261b3d9a4903 test/fixtures/users.yml --- a/test/fixtures/users.yml Fri Jun 14 09:05:06 2013 +0100 +++ b/test/fixtures/users.yml Tue Jan 14 14:37:42 2014 +0000 @@ -29,7 +29,7 @@ admin: true mail: admin@somenet.foo lastname: Admin - firstname: redMine + firstname: Redmine id: 1 auth_source_id: mail_notification: all @@ -105,12 +105,15 @@ 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: '' - hashed_password: 1 + 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/account_controller_openid_test.rb --- a/test/functional/account_controller_openid_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/account_controller_openid_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -40,17 +40,17 @@ :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', @@ -60,11 +60,11 @@ :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' @@ -74,7 +74,7 @@ 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' @@ -82,18 +82,18 @@ 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' @@ -102,23 +102,23 @@ 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' @@ -131,6 +131,16 @@ assert_select 'input[name=?][value=?]', 'user[identity_url]', 'http://openid.example.com/good_blank_user' end + def test_post_login_should_not_verify_token_when_using_open_id + ActionController::Base.allow_forgery_protection = true + AccountController.any_instance.stubs(:using_open_id?).returns(true) + AccountController.any_instance.stubs(:authenticate_with_open_id).returns(true) + post :login + assert_response 200 + ensure + ActionController::Base.allow_forgery_protection = false + end + def test_register_after_login_failure_should_not_require_user_to_enter_a_password Setting.self_registration = '3' diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/account_controller_test.rb --- a/test/functional/account_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/account_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # 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 @@ -40,6 +33,14 @@ 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 post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1' @@ -62,6 +63,36 @@ 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 @@ -79,9 +110,23 @@ 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 - get :logout + post :logout assert_redirected_to '/' assert_nil @request.session[:user_id] end @@ -90,7 +135,7 @@ @controller.expects(:reset_session).once @request.session[:user_id] = 2 - get :logout + post :logout assert_response 302 end @@ -101,8 +146,21 @@ assert_template 'register' assert_not_nil assigns(:user) - assert_tag 'input', :attributes => {:name => 'user[password]'} - assert_tag 'input', :attributes => {:name => 'user[password_confirmation]'} + 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 @@ -194,6 +252,15 @@ 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 @@ -251,4 +318,27 @@ 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 038ba2d95de8 -r 261b3d9a4903 test/functional/activities_controller_test.rb --- a/test/functional/activities_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/activities_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class ActivitiesControllerTest < ActionController::TestCase @@ -9,7 +26,6 @@ :members, :groups_users, :enabled_modules, - :workflows, :journals, :journal_details @@ -19,16 +35,8 @@ assert_template 'index' assert_not_nil assigns(:events_by_day) - assert_tag :tag => "h3", - :content => /#{2.days.ago.to_date.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue-edit/ }, - :child => { :tag => "a", - :content => /(#{IssueStatus.find(2).name})/, - } - } - } + assert_select 'h3', :text => /#{2.days.ago.to_date.day}/ + assert_select 'dl dt.issue-edit a', :text => /(#{IssueStatus.find(2).name})/ end def test_project_index_with_invalid_project_id_should_respond_404 @@ -42,16 +50,8 @@ assert_template 'index' assert_not_nil assigns(:events_by_day) - assert_tag :tag => "h3", - :content => /#{3.day.ago.to_date.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue/ }, - :child => { :tag => "a", - :content => /Can't print recipes/, - } - } - } + assert_select 'h3', :text => /#{3.days.ago.to_date.day}/ + assert_select 'dl dt.issue a', :text => /Can't print recipes/ end def test_global_index @@ -63,16 +63,9 @@ i5 = Issue.find(5) d5 = User.find(1).time_to_date(i5.created_on) - assert_tag :tag => "h3", - :content => /#{d5.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue/ }, - :child => { :tag => "a", - :content => /Subproject issue/, - } - } - } + + assert_select 'h3', :text => /#{d5.day}/ + assert_select 'dl dt.issue a', :text => /Subproject issue/ end def test_user_index @@ -87,16 +80,8 @@ i1 = Issue.find(1) d1 = User.find(1).time_to_date(i1.created_on) - assert_tag :tag => "h3", - :content => /#{d1.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue/ }, - :child => { :tag => "a", - :content => /Can't print recipes/, - } - } - } + assert_select 'h3', :text => /#{d1.day}/ + assert_select 'dl dt.issue a', :text => /Can't print recipes/ end def test_user_index_with_invalid_user_id_should_respond_404 @@ -109,14 +94,13 @@ assert_response :success assert_template 'common/feed' - assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, - :attributes => {:rel => 'self', :href => 'http://test.host/activity.atom?with_subprojects=0'} - assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, - :attributes => {:rel => 'alternate', :href => 'http://test.host/activity?with_subprojects=0'} - - assert_tag :tag => 'entry', :child => { - :tag => 'link', - :attributes => {:href => 'http://test.host/issues/11'}} + assert_select 'feed' do + assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?with_subprojects=0' + assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?with_subprojects=0' + assert_select 'entry' do + assert_select 'link[href=?]', 'http://test.host/issues/11' + end + end end def test_index_atom_feed_with_explicit_selection @@ -133,21 +117,21 @@ assert_response :success assert_template 'common/feed' - assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, - :attributes => {:rel => 'self', :href => 'http://test.host/activity.atom?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'} - assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, - :attributes => {:rel => 'alternate', :href => 'http://test.host/activity?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'} - - assert_tag :tag => 'entry', :child => { - :tag => 'link', - :attributes => {:href => 'http://test.host/issues/11'}} + assert_select 'feed' do + assert_select 'link[rel=self][href=?]', 'http://test.host/activity.atom?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0' + assert_select 'link[rel=alternate][href=?]', 'http://test.host/activity?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0' + assert_select 'entry' do + assert_select 'link[href=?]', 'http://test.host/issues/11' + end + end end def test_index_atom_feed_with_one_item_type get :index, :format => 'atom', :show_issues => '1' assert_response :success assert_template 'common/feed' - assert_tag :tag => 'title', :content => /Issues/ + + assert_select 'title', :text => /Issues/ end def test_index_should_show_private_notes_with_permission_only diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/admin_controller_test.rb --- a/test/functional/admin_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/admin_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -27,15 +27,13 @@ def test_index get :index - assert_no_tag :tag => 'div', - :attributes => { :class => /nodata/ } + assert_select 'div.nodata', 0 end def test_index_with_no_configuration_data delete_configuration_data get :index - assert_tag :tag => 'div', - :attributes => { :class => /nodata/ } + assert_select 'div.nodata' end def test_projects @@ -85,12 +83,12 @@ def test_test_email user = User.find(1) - user.pref[:no_self_notified] = '1' + user.pref.no_self_notified = '1' user.pref.save! ActionMailer::Base.deliveries.clear get :test_email - assert_redirected_to '/settings/edit?tab=notifications' + assert_redirected_to '/settings?tab=notifications' mail = ActionMailer::Base.deliveries.last assert_not_nil mail user = User.find(1) @@ -100,7 +98,7 @@ def test_test_email_failure_should_display_the_error Mailer.stubs(:test_email).raises(Exception, 'Some error message') get :test_email - assert_redirected_to '/settings/edit?tab=notifications' + assert_redirected_to '/settings?tab=notifications' assert_match /Some error message/, flash[:error] end @@ -128,8 +126,14 @@ assert_response :success assert_template 'plugins' - assert_tag :td, :child => { :tag => 'span', :content => 'Foo plugin' } - assert_tag :td, :child => { :tag => 'span', :content => 'Bar' } + assert_select 'tr#plugin-foo' do + assert_select 'td span.name', :text => 'Foo plugin' + assert_select 'td.configure a[href=/settings/plugin/foo]' + end + assert_select 'tr#plugin-bar' do + assert_select 'td span.name', :text => 'Bar' + assert_select 'td.configure a', 0 + end end def test_info @@ -145,8 +149,7 @@ get :index assert_response :success - assert_tag :a, :attributes => { :href => '/foo/bar' }, - :content => 'Test' + assert_select 'div#admin-menu a[href=/foo/bar]', :text => 'Test' Redmine::MenuManager.map :admin_menu do |menu| menu.delete :test_admin_menu_plugin_extension diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/attachments_controller_test.rb --- a/test/functional/attachments_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/attachments_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,10 +18,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'attachments_controller' - -# Re-raise errors caught by the controller. -class AttachmentsController; def rescue_action(e) raise e end; end class AttachmentsControllerTest < ActionController::TestCase fixtures :users, :projects, :roles, :members, :member_roles, @@ -29,9 +25,6 @@ :versions, :wiki_pages, :wikis, :documents def setup - @controller = AttachmentsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil set_fixtures_attachments_directory end @@ -57,7 +50,7 @@ set_tmp_attachments_directory end - def test_show_diff_replcace_cannot_convert_content + def test_show_diff_replace_cannot_convert_content with_settings :repositories_encodings => 'UTF-8' do ['inline', 'sbs'].each do |dt| # 060719210727_changeset_iso8859-1.diff @@ -159,7 +152,7 @@ :sibling => { :tag => 'td', :content => /#{str_japanese}/ } end - def test_show_text_file_replcace_cannot_convert_content + def test_show_text_file_replace_cannot_convert_content set_tmp_attachments_directory with_settings :repositories_encodings => 'UTF-8' do a = Attachment.new(:container => Issue.find(1), @@ -230,12 +223,21 @@ set_tmp_attachments_directory end - def test_show_file_without_container_should_be_denied + def test_show_file_without_container_should_be_allowed_to_author set_tmp_attachments_directory attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) @request.session[:user_id] = 2 get :show, :id => attachment.id + assert_response 200 + end + + def test_show_file_without_container_should_be_denied_to_other_users + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + + @request.session[:user_id] = 3 + get :show, :id => attachment.id assert_response 403 end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/auth_sources_controller_test.rb --- a/test/functional/auth_sources_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/auth_sources_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -42,8 +42,15 @@ assert_equal AuthSourceLdap, source.class assert source.new_record? - assert_tag 'input', :attributes => {:name => 'type', :value => 'AuthSourceLdap'} - assert_tag 'input', :attributes => {:name => 'auth_source[host]'} + assert_select 'form#auth_source_form' do + assert_select 'input[name=type][value=AuthSourceLdap]' + assert_select 'input[name=?]', 'auth_source[host]' + end + end + + def test_new_with_invalid_type_should_respond_with_404 + get :new, :type => 'foo' + assert_response 404 end def test_create @@ -52,7 +59,7 @@ assert_redirected_to '/auth_sources' end - source = AuthSourceLdap.first(:order => 'id DESC') + source = AuthSourceLdap.order('id DESC').first assert_equal 'Test', source.name assert_equal '127.0.0.1', source.host assert_equal 389, source.port @@ -74,7 +81,23 @@ assert_response :success assert_template 'edit' - assert_tag 'input', :attributes => {:name => 'auth_source[host]'} + assert_select 'form#auth_source_form' do + assert_select 'input[name=?]', 'auth_source[host]' + end + end + + def test_edit_should_not_contain_password + AuthSource.find(1).update_column :account_password, 'secret' + + get :edit, :id => 1 + assert_response :success + assert_select 'input[value=secret]', 0 + assert_select 'input[name=dummy_password][value=?]', /x+/ + end + + def test_edit_invalid_should_respond_with_404 + get :edit, :id => 99 + assert_response 404 end def test_update @@ -96,6 +119,7 @@ def test_destroy assert_difference 'AuthSourceLdap.count', -1 do delete :destroy, :id => 1 + assert_redirected_to '/auth_sources' end end @@ -104,6 +128,7 @@ assert_no_difference 'AuthSourceLdap.count' do delete :destroy, :id => 1 + assert_redirected_to '/auth_sources' end end @@ -124,4 +149,20 @@ assert_not_nil flash[:error] assert_include 'Something went wrong', flash[:error] end + + def test_autocomplete_for_new_user + AuthSource.expects(:search).with('foo').returns([ + {:login => 'foo1', :firstname => 'John', :lastname => 'Smith', :mail => 'foo1@example.net', :auth_source_id => 1}, + {:login => 'Smith', :firstname => 'John', :lastname => 'Doe', :mail => 'foo2@example.net', :auth_source_id => 1} + ]) + + get :autocomplete_for_new_user, :term => 'foo' + assert_response :success + assert_equal 'application/json', response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + assert_equal 2, json.size + assert_equal 'foo1', json.first['value'] + assert_equal 'foo1 (John Smith)', json.first['label'] + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/auto_completes_controller_test.rb --- a/test/functional/auto_completes_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/auto_completes_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class AutoCompletesControllerTest < ActionController::TestCase @@ -9,7 +26,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :journals, :journal_details def test_issues_should_not_be_case_sensitive @@ -33,6 +49,13 @@ assert assigns(:issues).include?(Issue.find(13)) end + def test_issues_should_return_issue_with_given_id_preceded_with_hash + get :issues, :project_id => 'subproject1', :q => '#13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + def test_auto_complete_with_scope_all_should_search_other_projects get :issues, :project_id => 'ecookbook', :q => '13', :scope => 'all' assert_response :success diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/boards_controller_test.rb --- a/test/functional/boards_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/boards_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -90,8 +90,9 @@ assert_response :success assert_template 'show' - assert_tag 'form', :attributes => {:id => 'message-form'} - assert_tag 'input', :attributes => {:name => 'message[subject]'} + assert_select 'form#message-form' do + assert_select 'input[name=?]', 'message[subject]' + end end def test_show_atom @@ -116,7 +117,7 @@ assert_select 'select[name=?]', 'board[parent_id]' do assert_select 'option', (Project.find(1).boards.size + 1) - assert_select 'option[value=]', :text => '' + assert_select 'option[value=]', :text => ' ' assert_select 'option[value=1]', :text => 'Help' end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/calendars_controller_test.rb --- a/test/functional/calendars_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/calendars_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class CalendarsControllerTest < ActionController::TestCase @@ -9,13 +26,20 @@ :members, :enabled_modules - def test_calendar + def test_show get :show, :project_id => 1 assert_response :success assert_template 'calendar' assert_not_nil assigns(:calendar) end + def test_show_should_run_custom_queries + @query = IssueQuery.create!(:name => 'Calendar', :visibility => IssueQuery::VISIBILITY_PUBLIC) + + get :show, :query_id => @query.id + assert_response :success + end + def test_cross_project_calendar get :show assert_response :success @@ -23,58 +47,38 @@ assert_not_nil assigns(:calendar) end - context "GET :show" do - should "run custom queries" do - @query = Query.create!(:name => 'Calendar', :is_public => true) - - get :show, :query_id => @query.id - assert_response :success - end - - end - def test_week_number_calculation Setting.start_of_week = 7 get :show, :month => '1', :year => '2010' assert_response :success - assert_tag :tag => 'tr', - :descendant => {:tag => 'td', - :attributes => {:class => 'week-number'}, :content => '53'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'odd'}, :content => '27'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'even'}, :content => '2'} + assert_select 'tr' do + assert_select 'td.week-number', :text => '53' + assert_select 'td.odd', :text => '27' + assert_select 'td.even', :text => '2' + end - assert_tag :tag => 'tr', - :descendant => {:tag => 'td', - :attributes => {:class => 'week-number'}, :content => '1'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'odd'}, :content => '3'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'even'}, :content => '9'} - + assert_select 'tr' do + assert_select 'td.week-number', :text => '1' + assert_select 'td.odd', :text => '3' + assert_select 'td.even', :text => '9' + end Setting.start_of_week = 1 get :show, :month => '1', :year => '2010' assert_response :success - assert_tag :tag => 'tr', - :descendant => {:tag => 'td', - :attributes => {:class => 'week-number'}, :content => '53'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'even'}, :content => '28'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'even'}, :content => '3'} + assert_select 'tr' do + assert_select 'td.week-number', :text => '53' + assert_select 'td.even', :text => '28' + assert_select 'td.even', :text => '3' + end - assert_tag :tag => 'tr', - :descendant => {:tag => 'td', - :attributes => {:class => 'week-number'}, :content => '1'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'even'}, :content => '4'}, - :descendant => {:tag => 'td', - :attributes => {:class => 'even'}, :content => '10'} - + assert_select 'tr' do + assert_select 'td.week-number', :text => '1' + assert_select 'td.even', :text => '4' + assert_select 'td.even', :text => '10' + end end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/comments_controller_test.rb --- a/test/functional/comments_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/comments_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/context_menus_controller_test.rb --- a/test/functional/context_menus_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/context_menus_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class ContextMenusControllerTest < ActionController::TestCase @@ -21,34 +38,21 @@ get :issues, :ids => [1] assert_response :success assert_template 'context_menu' - assert_tag :tag => 'a', :content => 'Edit', - :attributes => { :href => '/issues/1/edit', - :class => 'icon-edit' } - assert_tag :tag => 'a', :content => 'Closed', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bstatus_id%5D=5', - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', - :class => '' } - assert_no_tag :tag => 'a', :content => 'Inactive Priority' + + assert_select 'a.icon-edit[href=?]', '/issues/1/edit', :text => 'Edit' + assert_select 'a.icon-copy[href=?]', '/projects/ecookbook/issues/1/copy', :text => 'Copy' + assert_select 'a.icon-del[href=?]', '/issues?ids%5B%5D=1', :text => 'Delete' + + # Statuses + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bstatus_id%5D=5', :text => 'Closed' + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', :text => 'Immediate' + # No inactive priorities + assert_select 'a', :text => /Inactive Priority/, :count => 0 # Versions - assert_tag :tag => 'a', :content => '2.0', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', - :class => '' } - assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', - :class => '' } - - assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', - :class => '' } - assert_tag :tag => 'a', :content => 'Copy', - :attributes => { :href => '/projects/ecookbook/issues/1/copy', - :class => 'icon-copy' } - assert_no_tag :tag => 'a', :content => 'Move' - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => '/issues?ids%5B%5D=1', - :class => 'icon-del' } + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', :text => '2.0' + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0' + # Assignees + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper' end def test_context_menu_one_issue_by_anonymous @@ -69,25 +73,14 @@ assert_equal [1, 2], assigns(:issues).map(&:id).sort ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&') - assert_tag :tag => 'a', :content => 'Edit', - :attributes => { :href => "/issues/bulk_edit?#{ids}", - :class => 'icon-edit' } - assert_tag :tag => 'a', :content => 'Closed', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", - :class => '' } - assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", - :class => '' } - assert_tag :tag => 'a', :content => 'Copy', - :attributes => { :href => "/issues/bulk_edit?copy=1&#{ids}", - :class => 'icon-copy' } - assert_no_tag :tag => 'a', :content => 'Move' - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => "/issues?#{ids}", - :class => 'icon-del' } + + assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit' + assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&#{ids}", :text => 'Copy' + assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete' + + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper' end def test_context_menu_multiple_issues_of_different_projects @@ -99,21 +92,13 @@ assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&') - assert_tag :tag => 'a', :content => 'Edit', - :attributes => { :href => "/issues/bulk_edit?#{ids}", - :class => 'icon-edit' } - assert_tag :tag => 'a', :content => 'Closed', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", - :class => '' } - assert_tag :tag => 'a', :content => 'John Smith', - :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=2", - :class => '' } - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => "/issues?#{ids}", - :class => 'icon-del' } + + assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit' + assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete' + + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", :text => 'Closed' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", :text => 'Immediate' + assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=2", :text => 'John Smith' end def test_context_menu_should_include_list_custom_fields @@ -122,17 +107,14 @@ @request.session[:user_id] = 2 get :issues, :ids => [1] - assert_tag 'a', - :content => 'List', - :attributes => {:href => '#'}, - :sibling => {:tag => 'ul', :children => {:count => 3}} - - assert_tag 'a', - :content => 'Foo', - :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo"} - assert_tag 'a', - :content => 'none', - :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__"} + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'List' + assert_select 'ul' do + assert_select 'a', 3 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end end def test_context_menu_should_not_include_null_value_for_required_custom_fields @@ -141,10 +123,13 @@ @request.session[:user_id] = 2 get :issues, :ids => [1, 2] - assert_tag 'a', - :content => 'List', - :attributes => {:href => '#'}, - :sibling => {:tag => 'ul', :children => {:count => 2}} + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'List' + assert_select 'ul' do + assert_select 'a', 2 + assert_select 'a', :text => 'none', :count => 0 + end + end end def test_context_menu_on_single_issue_should_select_current_custom_field_value @@ -156,13 +141,13 @@ @request.session[:user_id] = 2 get :issues, :ids => [1] - assert_tag 'a', - :content => 'List', - :attributes => {:href => '#'}, - :sibling => {:tag => 'ul', :children => {:count => 3}} - assert_tag 'a', - :content => 'Bar', - :attributes => {:class => /icon-checked/} + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'List' + assert_select 'ul' do + assert_select 'a', 3 + assert_select 'a.icon-checked', :text => 'Bar' + end + end end def test_context_menu_should_include_bool_custom_fields @@ -171,14 +156,15 @@ @request.session[:user_id] = 2 get :issues, :ids => [1] - assert_tag 'a', - :content => 'Bool', - :attributes => {:href => '#'}, - :sibling => {:tag => 'ul', :children => {:count => 3}} - - assert_tag 'a', - :content => 'Yes', - :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1"} + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'Bool' + assert_select 'ul' do + assert_select 'a', 3 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end end def test_context_menu_should_include_user_custom_fields @@ -187,14 +173,14 @@ @request.session[:user_id] = 2 get :issues, :ids => [1] - assert_tag 'a', - :content => 'User', - :attributes => {:href => '#'}, - :sibling => {:tag => 'ul', :children => {:count => Project.find(1).members.count + 1}} - - assert_tag 'a', - :content => 'John Smith', - :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2"} + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'User' + assert_select 'ul' do + assert_select 'a', Project.find(1).members.count + 1 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end end def test_context_menu_should_include_version_custom_fields @@ -202,14 +188,14 @@ @request.session[:user_id] = 2 get :issues, :ids => [1] - assert_tag 'a', - :content => 'Version', - :attributes => {:href => '#'}, - :sibling => {:tag => 'ul', :children => {:count => Project.find(1).shared_versions.count + 1}} - - assert_tag 'a', - :content => '2.0', - :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3"} + assert_select "li.cf_#{field.id}" do + assert_select 'a[href=#]', :text => 'Version' + assert_select 'ul' do + assert_select 'a', Project.find(1).shared_versions.count + 1 + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0' + assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none' + end + end end def test_context_menu_by_assignable_user_should_include_assigned_to_me_link @@ -218,9 +204,7 @@ assert_response :success assert_template 'context_menu' - assert_tag :tag => 'a', :content => / me /, - :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=2', - :class => '' } + assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=2', :text => / me / end def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects @@ -232,25 +216,28 @@ assert_template 'context_menu' assert_include version, assigns(:versions) - assert_tag :tag => 'a', :content => 'eCookbook - Shared' + assert_select 'a', :text => 'eCookbook - Shared' end - def test_context_menu_issue_visibility - get :issues, :ids => [1, 4] - assert_response :success - assert_template 'context_menu' - assert_equal [1], assigns(:issues).collect(&:id) + def test_context_menu_with_issue_that_is_not_visible_should_fail + get :issues, :ids => [1, 4] # issue 4 is not visible + assert_response 302 end - + + def test_should_respond_with_404_without_ids + get :issues + assert_response 404 + end + def test_time_entries_context_menu @request.session[:user_id] = 2 get :time_entries, :ids => [1, 2] assert_response :success assert_template 'time_entries' - assert_tag 'a', :content => 'Edit' - assert_no_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/} + + assert_select 'a:not(.disabled)', :text => 'Edit' end - + def test_time_entries_context_menu_without_edit_permission @request.session[:user_id] = 2 Role.find_by_name('Manager').remove_permission! :edit_time_entries @@ -258,6 +245,6 @@ get :time_entries, :ids => [1, 2] assert_response :success assert_template 'time_entries' - assert_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/} + assert_select 'a.disabled', :text => 'Edit' end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/custom_fields_controller_test.rb --- a/test/functional/custom_fields_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/custom_fields_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,7 +18,7 @@ require File.expand_path('../../test_helper', __FILE__) class CustomFieldsControllerTest < ActionController::TestCase - fixtures :custom_fields, :custom_values, :trackers, :users + fixtures :custom_fields, :custom_values, :trackers, :users, :projects def setup @request.session[:user_id] = 1 @@ -52,10 +52,46 @@ 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 @@ -94,6 +130,17 @@ 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 => ''} @@ -129,7 +176,7 @@ end def test_destroy - custom_values_count = CustomValue.count(:conditions => {:custom_field_id => 1}) + custom_values_count = CustomValue.where(:custom_field_id => 1).count assert custom_values_count > 0 assert_difference 'CustomField.count', -1 do diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/documents_controller_test.rb --- a/test/functional/documents_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/documents_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/enumerations_controller_test.rb --- a/test/functional/enumerations_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/enumerations_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -117,7 +117,7 @@ end def test_destroy_enumeration_in_use_with_reassignment - issue = Issue.find(:first, :conditions => {:priority_id => 4}) + issue = Issue.where(:priority_id => 4).first assert_difference 'IssuePriority.count', -1 do delete :destroy, :id => 4, :reassign_to_id => 6 end @@ -126,4 +126,11 @@ # check that the issue was reassign assert_equal 6, issue.reload.priority_id end + + def test_destroy_enumeration_in_use_with_blank_reassignment + assert_no_difference 'IssuePriority.count' do + delete :destroy, :id => 4, :reassign_to_id => '' + end + assert_response :success + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/files_controller_test.rb --- a/test/functional/files_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/files_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class FilesControllerTest < ActionController::TestCase @@ -8,15 +25,11 @@ :member_roles, :members, :enabled_modules, - :workflows, :journals, :journal_details, :attachments, :versions def setup - @controller = FilesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new @request.session[:user_id] = nil Setting.default_language = 'en' end @@ -68,7 +81,7 @@ end end assert_redirected_to '/projects/ecookbook/files' - a = Attachment.find(:first, :order => 'created_on DESC') + a = Attachment.order('created_on DESC').first assert_equal 'testfile.txt', a.filename assert_equal Project.find(1), a.container @@ -88,7 +101,7 @@ assert_response :redirect end assert_redirected_to '/projects/ecookbook/files' - a = Attachment.find(:first, :order => 'created_on DESC') + a = Attachment.order('created_on DESC').first assert_equal 'testfile.txt', a.filename assert_equal Version.find(2), a.container end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/gantts_controller_test.rb --- a/test/functional/gantts_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/gantts_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,83 +25,98 @@ :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 + 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_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}/ + assert_equal 'image/png', @response.content_type 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 end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/groups_controller_test.rb --- a/test/functional/groups_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/groups_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -79,8 +79,11 @@ get :edit, :id => 10 assert_response :success assert_template 'edit' - assert_tag 'div', :attributes => {:id => 'tab-content-users'} - assert_tag 'div', :attributes => {:id => 'tab-content-memberships'} + + assert_select 'div#tab-content-users' + assert_select 'div#tab-content-memberships' do + assert_select 'a', :text => 'Private child of eCookbook' + end end def test_update @@ -192,11 +195,8 @@ end def test_autocomplete_for_user - get :autocomplete_for_user, :id => 10, :q => 'mis' + get :autocomplete_for_user, :id => 10, :q => 'smi', :format => 'js' assert_response :success - users = assigns(:users) - assert_not_nil users - assert users.any? - assert !users.include?(Group.find(10).users.first) + assert_include 'John Smith', response.body end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/issue_categories_controller_test.rb --- a/test/functional/issue_categories_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/issue_categories_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,19 +16,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'issue_categories_controller' - -# Re-raise errors caught by the controller. -class IssueCategoriesController; def rescue_action(e) raise e end; end class IssueCategoriesControllerTest < ActionController::TestCase fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules, :issue_categories, :issues def setup - @controller = IssueCategoriesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil @request.session[:user_id] = 2 end @@ -133,7 +126,7 @@ end def test_destroy_category_in_use_with_reassignment - issue = Issue.find(:first, :conditions => {:category_id => 1}) + 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) @@ -142,7 +135,7 @@ end def test_destroy_category_in_use_without_reassignment - issue = Issue.find(:first, :conditions => {:category_id => 1}) + 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) diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/issue_relations_controller_test.rb --- a/test/functional/issue_relations_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/issue_relations_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -28,7 +28,8 @@ :issue_relations, :enabled_modules, :enumerations, - :trackers + :trackers, + :projects_trackers def setup User.current = nil @@ -87,6 +88,17 @@ 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) @@ -97,8 +109,6 @@ end end - should "prevent relation creation when there's a circular dependency" - 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 => ''} diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/issue_statuses_controller_test.rb --- a/test/functional/issue_statuses_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/issue_statuses_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,17 +1,26 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) -require 'issue_statuses_controller' - -# Re-raise errors caught by the controller. -class IssueStatusesController; def rescue_action(e) raise e end; end - class IssueStatusesControllerTest < ActionController::TestCase fixtures :issue_statuses, :issues, :users def setup - @controller = IssueStatusesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil @request.session[:user_id] = 1 # admin end @@ -45,7 +54,7 @@ post :create, :issue_status => {:name => 'New status'} end assert_redirected_to :action => 'index' - status = IssueStatus.find(:first, :order => 'id DESC') + status = IssueStatus.order('id DESC').first assert_equal 'New status', status.name end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/issues_controller_test.rb --- a/test/functional/issues_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/issues_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'issues_controller' class IssuesControllerTest < ActionController::TestCase fixtures :projects, @@ -48,9 +47,6 @@ include Redmine::I18n def setup - @controller = IssuesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end @@ -297,7 +293,7 @@ end end - def test_index_with_query_grouped_by_tracker + 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' @@ -331,7 +327,7 @@ end def test_index_with_cross_project_query_in_session_should_show_project_issues - q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil) + 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 @@ -345,7 +341,7 @@ end def test_private_query_should_not_be_available_to_other_users - q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil) + 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 @@ -353,7 +349,7 @@ end def test_private_query_should_be_available_to_its_user - q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil) + 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 @@ -361,7 +357,7 @@ end def test_public_query_should_be_available_to_other_users - q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil) + 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 @@ -384,7 +380,7 @@ 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 + 1, lines[0].split(',').size + assert_equal assigns(:query).columns.size, lines[0].split(',').size end def test_index_csv_with_project @@ -395,13 +391,18 @@ end def test_index_csv_with_description - get :index, :format => 'csv', :description => '1' - 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 + 2, lines[0].split(',').size + 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 @@ -420,9 +421,9 @@ 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).available_inline_columns.size + 1, lines[0].split(',').size + 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 @@ -437,6 +438,25 @@ 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" @@ -457,8 +477,8 @@ if str_utf8.respond_to?(:force_encoding) s1.force_encoding('Big5') end - assert lines[0].include?(s1) - assert lines[1].include?(str_big5) + assert_include s1, lines[0] + assert_include str_big5, lines[1] end end @@ -670,7 +690,7 @@ # query should use specified columns query = assigns(:query) - assert_kind_of Query, query + assert_kind_of IssueQuery, query assert_equal columns, query.column_names.map(&:to_s) # columns should be stored in session @@ -692,18 +712,18 @@ # query should use specified columns query = assigns(:query) - assert_kind_of Query, query - assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name) + 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 = ['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 Query, query + assert_kind_of IssueQuery, query assert_equal columns.map(&:to_sym), query.columns.map(&:name) end @@ -714,7 +734,7 @@ # query should use specified columns query = assigns(:query) - assert_kind_of Query, query + assert_kind_of IssueQuery, query assert_equal columns, query.column_names.map(&:to_s) assert_select 'table.issues td.cf_2.string' @@ -753,18 +773,14 @@ 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%;' @@ -774,20 +790,17 @@ 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 @@ -857,9 +870,7 @@ 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 @@ -867,7 +878,6 @@ assert_select 'textarea[name=?]', 'issue[notes]' end end - assert_select 'title', :text => "Bug #1: Can't print recipes - eCookbook - Redmine" end @@ -875,9 +885,7 @@ @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' @@ -899,24 +907,25 @@ get :show, :id => 1 assert_response :success - assert_tag 'form', :attributes => {:id => 'issue-form'} - assert_tag 'input', :attributes => {:name => 'issue[is_private]'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'} - assert_tag 'input', :attributes => {:name => 'issue[subject]'} - assert_tag 'textarea', :attributes => {:name => 'issue[description]'} - assert_tag 'select', :attributes => {:name => 'issue[status_id]'} - assert_tag 'select', :attributes => {:name => 'issue[priority_id]'} - assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'} - assert_tag 'select', :attributes => {:name => 'issue[category_id]'} - assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'} - assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'} - assert_tag 'input', :attributes => {:name => 'issue[start_date]'} - assert_tag 'input', :attributes => {:name => 'issue[due_date]'} - assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'} - assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' } - assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'} - assert_tag 'textarea', :attributes => {:name => 'issue[notes]'} + 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 @@ -927,24 +936,25 @@ get :show, :id => 1 assert_response :success - assert_tag 'form', :attributes => {:id => 'issue-form'} - assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'} - assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[subject]'} - assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'} - assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'} - assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'} - assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'} - assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' } - assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'} - assert_tag 'textarea', :attributes => {:name => 'issue[notes]'} + 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 @@ -954,24 +964,25 @@ get :show, :id => 1 assert_response :success - assert_tag 'form', :attributes => {:id => 'issue-form'} - assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'} - assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[subject]'} - assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'} - assert_tag 'select', :attributes => {:name => 'issue[status_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'} - assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'} - assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'} - assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'} - assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'} - assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'} - assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' } - assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'} - assert_tag 'textarea', :attributes => {:name => 'issue[notes]'} + 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 @@ -1004,7 +1015,7 @@ get :show, :id => 1 assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do - assert_select 'input[type=file][name=?]', 'attachments[1][file]' + assert_select 'input[type=file][name=?]', 'attachments[dummy][file]' end end @@ -1140,7 +1151,7 @@ end def test_show_should_display_prev_next_links_with_saved_query_in_session - query = Query.create!(:name => 'test', :is_public => true, :user_id => 1, + 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} @@ -1232,7 +1243,7 @@ CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3') CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '') - query = Query.create!(:name => 'test', :is_public => true, :user_id => 1, :filters => {}, + 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} @@ -1420,33 +1431,41 @@ assert @response.body.starts_with?('%PDF') 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_tag 'input', :attributes => {:name => 'issue[is_private]'} - assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'} - assert_tag 'input', :attributes => {:name => 'issue[subject]'} - assert_tag 'textarea', :attributes => {:name => 'issue[description]'} - assert_tag 'select', :attributes => {:name => 'issue[status_id]'} - assert_tag 'select', :attributes => {:name => 'issue[priority_id]'} - assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'} - assert_tag 'select', :attributes => {:name => 'issue[category_id]'} - assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'} - assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'} - assert_tag 'input', :attributes => {:name => 'issue[start_date]'} - assert_tag 'input', :attributes => {:name => 'issue[due_date]'} - assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'} - assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' } - assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'} + 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_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } + assert_select 'select[name=?]', 'issue[priority_id]' do + assert_select 'option[value=15]', 0 + end end def test_get_new_with_minimal_permissions @@ -1458,22 +1477,24 @@ assert_response :success assert_template 'new' - assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'} - assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'} - assert_tag 'input', :attributes => {:name => 'issue[subject]'} - assert_tag 'textarea', :attributes => {:name => 'issue[description]'} - assert_tag 'select', :attributes => {:name => 'issue[status_id]'} - assert_tag 'select', :attributes => {:name => 'issue[priority_id]'} - assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'} - assert_tag 'select', :attributes => {:name => 'issue[category_id]'} - assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'} - assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'} - assert_tag 'input', :attributes => {:name => 'issue[start_date]'} - assert_tag 'input', :attributes => {:name => 'issue[due_date]'} - assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'} - assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' } - assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'} + 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 @@ -1541,26 +1562,25 @@ end def test_get_new_without_default_start_date_is_creation_date - Setting.default_issue_start_date_to_creation_date = 0 - - @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 + 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 - Setting.default_issue_start_date_to_creation_date = 1 - - @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 + 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 @@ -1568,8 +1588,7 @@ 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[1][file]' - assert_select 'input[name=?][maxlength=255]', 'attachments[1][description]' + assert_select 'input[name=?][type=file]', 'attachments[dummy][file]' end end @@ -1663,9 +1682,9 @@ assert_error_tag :content => /No tracker/ end - def test_update_new_form + def test_update_form_for_new_issue @request.session[:user_id] = 2 - xhr :post, :new, :project_id => 1, + xhr :post, :update_form, :project_id => 1, :issue => {:tracker_id => 2, :subject => 'This is the test_new issue', :description => 'This is the description', @@ -1682,14 +1701,14 @@ assert_equal 'This is the test_new issue', issue.subject end - def test_update_new_form_should_propose_transitions_based_on_initial_status + 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, :new, :project_id => 1, + xhr :post, :update_form, :project_id => 1, :issue => {:tracker_id => 1, :status_id => 5, :subject => 'This is an issue'} @@ -1720,7 +1739,7 @@ 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.find(:first, :conditions => {:custom_field_id => 2}) + v = issue.custom_values.where(:custom_field_id => 2).first assert_not_nil v assert_equal 'Value for field 2', v.value end @@ -1748,11 +1767,10 @@ end def test_post_create_without_start_date_and_default_start_date_is_not_creation_date - Setting.default_issue_start_date_to_creation_date = 0 - - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, + 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', @@ -1760,20 +1778,20 @@ :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 - 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 def test_post_create_without_start_date_and_default_start_date_is_creation_date - Setting.default_issue_start_date_to_creation_date = 1 - - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, + 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', @@ -1781,12 +1799,13 @@ :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 - 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 def test_post_create_and_continue @@ -2082,19 +2101,15 @@ assert_response :success assert_template 'new' - assert_tag :textarea, :attributes => { :name => 'issue[description]' }, - :content => "\nThis is a description" - assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, - :child => { :tag => 'option', :attributes => { :selected => 'selected', - :value => '6' }, - :content => 'High' } + 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_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' }, - :child => { :tag => 'option', :attributes => { :selected => 'selected', - :value => 'Oracle' }, - :content => 'Oracle' } - assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]', - :value => 'Value for field 2'} + 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 @@ -2107,9 +2122,9 @@ assert_response :success assert_template 'new' - assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '2', :checked => nil} - assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '3', :checked => 'checked'} - assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '8', :checked => 'checked'} + 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 @@ -2144,6 +2159,25 @@ 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 @@ -2163,8 +2197,8 @@ assert File.exists?(attachment.diskfile) assert_nil attachment.container - assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} - assert_tag 'span', :content => /testfile.txt/ + 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 @@ -2182,8 +2216,8 @@ end end - assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} - assert_tag 'span', :content => /testfile.txt/ + 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 @@ -2218,10 +2252,10 @@ get :new, :project_id => 1 assert_response :success assert_template 'new' - assert_tag :tag => 'select', - :attributes => {:name => 'issue[status_id]'}, - :children => {:count => 1}, - :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}} + assert_select 'select[name=?]', 'issue[status_id]' do + assert_select 'option', 1 + assert_select 'option[value=?]', IssueStatus.default.id.to_s + end end should "accept default status" do @@ -2349,13 +2383,13 @@ assert_equal orig.subject, assigns(:issue).subject assert assigns(:issue).copy? - assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'}, - :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}, :content => 'eCookbook'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'}, - :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'} - assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'} + 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]' @@ -2367,7 +2401,7 @@ assert issue.attachments.count > 0 get :new, :project_id => 1, :copy_from => 3 - assert_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'} + 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 @@ -2376,7 +2410,7 @@ issue.attachments.delete_all get :new, :project_id => 1, :copy_from => 3 - assert_no_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'} + assert_select 'input[name=copy_attachments]', 0 end def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox @@ -2415,12 +2449,12 @@ issue = Issue.find(3) count = issue.attachments.count assert count > 0 - assert_difference 'Issue.count' do assert_difference 'Attachment.count', count do - assert_no_difference 'Journal.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'}, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with attachments'}, :copy_attachments => '1' end end @@ -2435,12 +2469,12 @@ issue = Issue.find(3) count = issue.attachments.count assert count > 0 - assert_difference 'Issue.count' do assert_no_difference 'Attachment.count' do - assert_no_difference 'Journal.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'} + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with attachments'} end end end @@ -2453,14 +2487,16 @@ issue = Issue.find(3) count = issue.attachments.count assert count > 0 - assert_difference 'Issue.count' do assert_difference 'Attachment.count', count + 1 do - assert_no_difference 'Journal.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'}, + :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'}} + :attachments => {'1' => + {'file' => uploaded_test_file('testfile.txt', 'text/plain'), + 'description' => 'test file'}} end end end @@ -2470,11 +2506,11 @@ 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'} + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy'} end end copy = Issue.first(:order => 'id DESC') @@ -2485,11 +2521,11 @@ @request.session[:user_id] = 2 issue = Issue.generate_with_descendants! count = issue.descendants.count - - assert_difference 'Issue.count', count+1 do - assert_no_difference 'Journal.count' do + 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'}, + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with subtasks'}, :copy_subtasks => '1' end end @@ -2501,11 +2537,11 @@ 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_no_difference 'Journal.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 subtasks'} + :issue => {:project_id => '1', :tracker_id => '3', + :status_id => '1', :subject => 'Copy with subtasks'} end end copy = Issue.where(:parent_id => nil).first(:order => 'id DESC') @@ -2523,13 +2559,13 @@ assert_not_nil assigns(:issue) assert assigns(:issue).copy? - assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'}, - :child => {:tag => 'option', :attributes => {:value => '1', :selected => nil}, :content => 'eCookbook'} - assert_tag 'select', :attributes => {:name => 'issue[project_id]'}, - :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}, :content => 'OnlineStore'} - assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'} + 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 @@ -2554,8 +2590,9 @@ # Be sure we don't display inactive IssuePriorities assert ! IssuePriority.find(15).active? - assert_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } + 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 @@ -2563,7 +2600,7 @@ Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time] get :edit, :id => 1 - assert_tag 'input', :attributes => {:name => 'time_entry[hours]'} + assert_select 'input[name=?]', 'time_entry[hours]' end def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission @@ -2571,13 +2608,13 @@ Role.find_by_name('Manager').remove_permission! :log_time get :edit, :id => 1 - assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'} + 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 => TimeEntryActivity.first.id } + :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => 10 } assert_response :success assert_template 'edit' @@ -2585,22 +2622,20 @@ assert_not_nil issue assert_equal 5, issue.status_id - assert_tag :select, :attributes => { :name => 'issue[status_id]' }, - :child => { :tag => 'option', - :content => 'Closed', - :attributes => { :selected => 'selected' } } + 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_tag :select, :attributes => { :name => 'issue[priority_id]' }, - :child => { :tag => 'option', - :content => 'Urgent', - :attributes => { :selected => 'selected' } } - - assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' } - assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' }, - :child => { :tag => 'option', - :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } } - assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' } + 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 @@ -2615,18 +2650,17 @@ assert_response :success assert_template 'edit' - assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'} - assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'}, - :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}} - assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'}, - :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}} - assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'}, - :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}} - end - - def test_update_edit_form + 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, :new, :project_id => 1, + xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:tracker_id => 2, :subject => 'This is the test_new issue', @@ -2645,9 +2679,9 @@ assert_equal 'This is the test_new issue', issue.subject end - def test_update_edit_form_should_keep_issue_author + def test_update_form_for_existing_issue_should_keep_issue_author @request.session[:user_id] = 3 - xhr :put, :new, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'} + xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'} assert_response :success assert_equal 'text/javascript', response.content_type @@ -2657,14 +2691,14 @@ assert_not_equal User.current, issue.author end - def test_update_edit_form_should_propose_transitions_based_on_initial_status + 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, :new, :project_id => 1, + xhr :put, :update_form, :project_id => 1, :id => 2, :issue => {:tracker_id => 2, :status_id => 5, @@ -2674,9 +2708,9 @@ assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort end - def test_update_edit_form_with_project_change + def test_update_form_for_existing_issue_with_project_change @request.session[:user_id] = 2 - xhr :put, :new, :project_id => 1, + xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:project_id => 2, :tracker_id => 2, @@ -2694,6 +2728,16 @@ 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 @@ -2831,7 +2875,7 @@ assert_redirected_to :action => 'show', :id => '1' issue.reload assert_equal 2, issue.status_id - j = Journal.find(:first, :order => 'id DESC') + j = Journal.order('id DESC').first assert_equal 'Assigned to dlopper', j.notes assert_equal 2, j.details.size @@ -2848,7 +2892,7 @@ :id => 1, :issue => { :notes => notes } assert_redirected_to :action => 'show', :id => '1' - j = Journal.find(:first, :order => 'id DESC') + j = Journal.order('id DESC').first assert_equal notes, j.notes assert_equal 0, j.details.size assert_equal User.anonymous, j.user @@ -2904,7 +2948,7 @@ issue = Issue.find(1) - j = Journal.find(:first, :order => 'id DESC') + j = Journal.order('id DESC').first assert_equal '2.5 hours added', j.notes assert_equal 0, j.details.size @@ -2943,7 +2987,7 @@ end assert_redirected_to :action => 'show', :id => '1' - j = Issue.find(1).journals.find(:first, :order => 'id DESC') + 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 @@ -2982,8 +3026,8 @@ assert File.exists?(attachment.diskfile) assert_nil attachment.container - assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} - assert_tag 'span', :content => /testfile.txt/ + 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 @@ -3001,8 +3045,8 @@ end end - assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} - assert_tag 'span', :content => /testfile.txt/ + 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 @@ -3092,8 +3136,8 @@ assert_template 'edit' assert_error_tag :descendant => {:content => /Activity can't be blank/} - assert_tag :textarea, :attributes => { :name => 'issue[notes]' }, :content => "\n"+notes - assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" } + 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 @@ -3111,8 +3155,8 @@ assert_error_tag :descendant => {:content => /Activity can't be blank/} assert_error_tag :descendant => {:content => /Hours can't be blank/} - assert_tag :textarea, :attributes => { :name => 'issue[notes]' }, :content => "\n"+notes - assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" } + 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 @@ -3167,23 +3211,34 @@ assert_response :success assert_template 'bulk_edit' - assert_tag :select, :attributes => {:name => 'issue[project_id]'} - assert_tag :input, :attributes => {: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_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'} - - # System wide custom field - assert CustomField.find(1).is_for_all? - assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'} - - # Be sure we don't display inactive IssuePriorities - assert ! IssuePriority.find(15).active? - assert_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } + 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 @@ -3193,13 +3248,13 @@ assert_template 'bulk_edit' # Can not set issues from different projects as children of an issue - assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'} + 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_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'} + assert_select 'input[name=?]', 'issue[custom_field_values][9]', 0 end def test_get_bulk_edit_with_user_custom_field @@ -3210,12 +3265,9 @@ assert_response :success assert_template 'bulk_edit' - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][#{field.id}]", :class => 'user_cf'}, - :children => { - :only => {:tag => 'option'}, - :count => Project.find(1).users.count + 2 # "no change" + "none" options - } + 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 @@ -3226,12 +3278,9 @@ assert_response :success assert_template 'bulk_edit' - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][#{field.id}]"}, - :children => { - :only => {:tag => 'option'}, - :count => Project.find(1).shared_versions.count + 2 # "no change" + "none" options - } + 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 @@ -3243,22 +3292,31 @@ assert_response :success assert_template 'bulk_edit' - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][1][]"}, - :children => { - :only => {:tag => 'option'}, - :count => field.possible_values.size + 1 # "none" options - } + 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) + 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] @@ -3267,8 +3325,9 @@ assert_not_nil statuses assert_equal [1, 3], statuses.map(&:id).sort - assert_tag 'select', :attributes => {:name => 'issue[status_id]'}, - :children => {:count => 3} # 2 statuses + "no change" option + 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 @@ -3277,9 +3336,10 @@ assert_response :success assert_template 'bulk_edit' assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort - assert_tag 'select', - :attributes => {:name => 'issue[fixed_version_id]'}, - :descendant => {:tag => 'option', :content => '2.0'} + + 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 @@ -3288,9 +3348,10 @@ assert_response :success assert_template 'bulk_edit' assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort - assert_tag 'select', - :attributes => {:name => 'issue[category_id]'}, - :descendant => {:tag => 'option', :content => 'Recipes'} + + assert_select 'select[name=?]', 'issue[category_id]' do + assert_select 'option', :text => 'Recipes' + end end def test_bulk_update @@ -3306,7 +3367,7 @@ assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} issue = Issue.find(1) - journal = issue.journals.find(:first, :order => 'created_on DESC') + 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 @@ -3341,7 +3402,7 @@ assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id) issue = Issue.find(1) - journal = issue.journals.find(:first, :order => 'created_on DESC') + 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 @@ -3443,11 +3504,12 @@ 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'} - + :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 @@ -3466,7 +3528,7 @@ assert_response 302 issue = Issue.find(1) - journal = issue.journals.find(:first, :order => 'created_on DESC') + 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 @@ -3555,13 +3617,44 @@ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier end - def test_bulk_update_with_failure_should_set_flash + def test_bulk_update_with_all_failures_should_show_errors @request.session[:user_id] = 2 - Issue.update_all("subject = ''", "id = 2") # Make it invalid - post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6} - - assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' - assert_equal 'Failed to save 1 issue(s) on 2 selected: #2.', flash[:error] + 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 @@ -3595,10 +3688,13 @@ 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) + 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 => { @@ -3621,7 +3717,7 @@ def test_bulk_copy_should_allow_changing_the_issue_attributes # Fixes random test failure with Mysql - # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) + # where Issue.where(:project_id => 2).limit(2).order('id desc') # doesn't return the expected results Issue.delete_all("project_id=2") @@ -3636,7 +3732,7 @@ end end - copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) + 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" @@ -3657,11 +3753,10 @@ :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31' } end - issue = Issue.first(:order => 'id DESC') assert_equal 1, issue.journals.size journal = issue.journals.first - assert_equal 0, journal.details.size + assert_equal 1, journal.details.size assert_equal 'Copying one issue', journal.notes end @@ -3757,6 +3852,13 @@ 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 @@ -3778,8 +3880,10 @@ assert_template 'destroy' assert_not_nil assigns(:hours) assert Issue.find_by_id(1) && Issue.find_by_id(3) - assert_tag 'form', - :descendant => {:tag => 'input', :attributes => {:name => '_method', :value => 'delete'}} + + assert_select 'form' do + assert_select 'input[name=_method][value=delete]' + end end def test_destroy_issues_and_destroy_time_entries @@ -3845,10 +3949,19 @@ 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_tag :div, :attributes => {:id => 'quick-search'}, - :child => {:tag => 'form', - :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}} + + assert_select 'div#quick-search form' do + assert_select 'input[name=issues][value=1][type=hidden]' + end end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/issues_controller_transaction_test.rb --- a/test/functional/issues_controller_transaction_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/issues_controller_transaction_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,6 +19,7 @@ require 'issues_controller' class IssuesControllerTransactionTest < ActionController::TestCase + tests IssuesController fixtures :projects, :users, :roles, @@ -46,9 +47,6 @@ self.use_transactional_fixtures = false def setup - @controller = IssuesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end @@ -106,7 +104,7 @@ 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/ + assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'} end def test_update_stale_issue_without_notes_should_not_show_add_notes_option @@ -254,7 +252,7 @@ end def test_index_should_rescue_invalid_sql_query - Query.any_instance.stubs(:statement).returns("INVALID STATEMENT") + IssueQuery.any_instance.stubs(:statement).returns("INVALID STATEMENT") get :index assert_response 500 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/issues_custom_fields_visibility_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/functional/issues_custom_fields_visibility_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,322 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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.find_all_by_id(1,3)) + User.add_to_project(user, p2, Role.find_all_by_id(3)) + 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 038ba2d95de8 -r 261b3d9a4903 test/functional/journals_controller_test.rb --- a/test/functional/journals_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/journals_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,19 +16,12 @@ # 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/mail_handler_controller_test.rb --- a/test/functional/mail_handler_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/mail_handler_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,10 +16,6 @@ # 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, @@ -28,9 +24,6 @@ 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/members_controller_test.rb --- a/test/functional/members_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/members_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,19 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'members_controller' - -# Re-raise errors caught by the controller. -class MembersController; def rescue_action(e) raise e end; end - class MembersControllerTest < ActionController::TestCase fixtures :projects, :members, :member_roles, :roles, :users def setup - @controller = MembersController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil @request.session[:user_id] = 2 end @@ -112,11 +104,8 @@ end def test_autocomplete - get :autocomplete, :project_id => 1, :q => 'mis' + get :autocomplete, :project_id => 1, :q => 'mis', :format => 'js' assert_response :success - assert_template 'autocomplete' - - assert_tag :label, :content => /User Misc/, - :child => { :tag => 'input', :attributes => { :name => 'membership[user_ids][]', :value => '8' } } + assert_include 'User Misc', response.body end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/messages_controller_test.rb --- a/test/functional/messages_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/messages_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # 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 @@ -93,6 +86,12 @@ assert_template 'new' end + def test_get_new_with_invalid_board + @request.session[:user_id] = 2 + get :new, :board_id => 99 + assert_response 404 + end + def test_post_new @request.session[:user_id] = 2 ActionMailer::Base.deliveries.clear @@ -164,7 +163,7 @@ 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') + reply = Message.order('id DESC').first assert_redirected_to "/boards/1/topics/1?r=#{reply.id}" assert Message.find_by_subject('Test reply') end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/my_controller_test.rb --- a/test/functional/my_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/my_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,20 +16,13 @@ # 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 @@ -58,6 +51,17 @@ 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/news_controller_test.rb --- a/test/functional/news_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/news_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'news_controller' - -# Re-raise errors caught by the controller. -class NewsController; def rescue_action(e) raise e end; end class NewsControllerTest < ActionController::TestCase fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments def setup - @controller = NewsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/previews_controller_test.rb --- a/test/functional/previews_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/previews_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,7 +25,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :journals, :journal_details, :news diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/project_enumerations_controller_test.rb --- a/test/functional/project_enumerations_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/project_enumerations_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class ProjectEnumerationsControllerTest < ActionController::TestCase @@ -8,7 +25,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, :time_entries @@ -75,14 +91,14 @@ project_activity = TimeEntryActivity.new({ :name => 'Project Specific', - :parent => TimeEntryActivity.find(:first), + :parent => TimeEntryActivity.first, :project => Project.find(1), :active => true }) assert project_activity.save project_activity_two = TimeEntryActivity.new({ :name => 'Project Specific Two', - :parent => TimeEntryActivity.find(:last), + :parent => TimeEntryActivity.last, :project => Project.find(1), :active => true }) @@ -156,14 +172,14 @@ @request.session[:user_id] = 2 # manager project_activity = TimeEntryActivity.new({ :name => 'Project Specific', - :parent => TimeEntryActivity.find(:first), + :parent => TimeEntryActivity.first, :project => Project.find(1), :active => true }) assert project_activity.save project_activity_two = TimeEntryActivity.new({ :name => 'Project Specific Two', - :parent => TimeEntryActivity.find(:last), + :parent => TimeEntryActivity.last, :project => Project.find(1), :active => true }) diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/projects_controller_test.rb --- a/test/functional/projects_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/projects_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,10 +16,6 @@ # 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, @@ -27,29 +23,27 @@ :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 + def test_index_by_anonymous_should_not_show_private_projects get :index assert_response :success assert_template 'index' - assert_not_nil assigns(:projects) + projects = assigns(:projects) + assert_not_nil projects + assert projects.all?(&:is_public?) - 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/ + assert_select 'ul' do + assert_select 'li' do + assert_select 'a', :text => 'eCookbook' + assert_select 'ul' do + assert_select 'a', :text => 'Child of private child' + end + end + end + assert_select 'a', :text => /Private child of eCookbook/, :count => 0 end def test_index_atom @@ -57,242 +51,240 @@ 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)) + assert_select 'feed>entry', :count => Project.visible(User.current).count 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 + test "#index by non-admin user with view_time_entries permission should show overall spent time link" do + @request.session[:user_id] = 3 + get :index + assert_template 'index' + assert_select 'a[href=?]', '/time_entries' + 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 + test "#index by non-admin user without view_time_entries permission should not show overall spent time link" 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 + + get :index + assert_template 'index' + assert_select 'a[href=?]', '/time_entries', 0 + end + + test "#new by admin user should accept get" do + @request.session[:user_id] = 1 + + get :new + assert_response :success + assert_template 'new' + end + + test "#new by non-admin user with add_project permission should accept get" do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + + get :new + assert_response :success + assert_template 'new' + assert_select 'select[name=?]', 'project[parent_id]', 0 + end + + test "#new by non-admin user with add_subprojects permission should accept get" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + get :new, :parent_id => 'ecookbook' + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'project[parent_id]' do + # parent project selected + assert_select 'option[value=1][selected=selected]' + # no empty value + assert_select 'option[value=]', 0 end end - context "#new" do - context "by admin user" do - setup do - @request.session[:user_id] = 1 - end + test "#create by admin user should create a new project" do + @request.session[:user_id] = 1 - should "accept get" do - get :new - assert_response :success - assert_template 'new' - end + 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 + + test "#create by admin user should create a new subproject" do + @request.session[:user_id] = 1 + + assert_difference 'Project.count' 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' 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 + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert_equal Project.find(1), project.parent + end - should "accept get" do - get :new - assert_response :success - assert_template 'new' - assert_no_tag :select, :attributes => {:name => 'project[parent_id]'} - end + test "#create by admin user should continue" do + @request.session[:user_id] = 1 + + assert_difference 'Project.count' do + post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue' + end + assert_redirected_to '/projects/new' + end + + test "#create by non-admin user with add_project permission should create a new project" do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + + 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 + + test "#create by non-admin user with add_project permission should fail with parent_id" do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + + 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_equal [], project.errors[:parent_id] + end + + test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + 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 + + test "#create by non-admin user with add_subprojects permission should fail without parent_id" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + 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_equal [], project.errors[:parent_id] + end + + test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + + 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_equal [], project.errors[:parent_id] + end + + def test_create_subproject_with_inherit_members_should_inherit_members + Role.find_by_name('Manager').add_permission! :add_subprojects + parent = Project.find(1) + @request.session[:user_id] = 2 + + assert_difference 'Project.count' do + post :create, :project => { + :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1' + } + assert_response 302 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 + project = Project.order('id desc').first + assert_equal 'inherited', project.name + assert_equal parent, project.parent + assert project.memberships.count > 0 + assert_equal parent.memberships.count, project.memberships.count end def test_create_should_preserve_modules_on_validation_failure @@ -325,7 +317,17 @@ assert_not_nil assigns(:project) assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) - assert_tag 'li', :content => /Development status/ + assert_select 'li', :text => /Development status/ + end + + def test_show_should_not_display_empty_sidebar + p = Project.find(1) + p.enabled_module_names = [] + p.save! + + get :show, :id => 'ecookbook' + assert_response :success + assert_select '#main.nosidebar' end def test_show_should_not_display_hidden_custom_fields @@ -335,7 +337,7 @@ assert_template 'show' assert_not_nil assigns(:project) - assert_no_tag 'li', :content => /Development status/ + assert_select 'li', :text => /Development status/, :count => 0 end def test_show_should_not_fail_when_custom_values_are_nil @@ -355,22 +357,22 @@ get :show, :id => 'ecookbook' assert_response 403 assert_nil assigns(:project) - assert_tag :tag => 'p', :content => /archived/ + assert_select 'p', :text => /archived/ end - def test_private_subprojects_hidden + def test_show_should_not_show_private_subprojects_that_are_not_visible get :show, :id => 'ecookbook' assert_response :success assert_template 'show' - assert_no_tag :tag => 'a', :content => /Private child/ + assert_select 'a', :text => /Private child/, :count => 0 end - def test_private_subprojects_visible + def test_show_should_show_private_subprojects_that_are_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/ + assert_select 'a', :text => /Private child/ end def test_settings @@ -380,6 +382,15 @@ assert_template 'settings' end + def test_settings_of_subproject + @request.session[:user_id] = 2 + get :settings, :id => 'private-child' + assert_response :success + assert_template 'settings' + + assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]' + end + def test_settings_should_be_denied_for_member_on_closed_project Project.find(1).close @request.session[:user_id] = 2 # manager @@ -438,22 +449,37 @@ assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort end - def test_destroy_without_confirmation + def test_destroy_leaf_project_without_confirmation_should_show_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', + + assert_no_difference 'Project.count' do + delete :destroy, :id => 2 + assert_response :success + assert_template 'destroy' + end + end + + def test_destroy_without_confirmation_should_show_confirmation_with_subprojects + @request.session[:user_id] = 1 # admin + + assert_no_difference 'Project.count' do + delete :destroy, :id => 1 + assert_response :success + assert_template 'destroy' + end + assert_select 'strong', + :text => ['Private child of eCookbook', 'Child of private child, eCookbook Subproject 1', 'eCookbook Subproject 2'].join(', ') end - def test_destroy + def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects @request.session[:user_id] = 1 # admin - delete :destroy, :id => 1, :confirm => 1 - assert_redirected_to '/admin/projects' + + assert_difference 'Project.count', -5 do + delete :destroy, :id => 1, :confirm => 1 + assert_redirected_to '/admin/projects' + end assert_nil Project.find_by_id(1) end @@ -499,12 +525,11 @@ CustomField.delete_all parent = nil 6.times do |i| - p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}") - p.set_parent!(parent) + p = Project.generate_with_parent!(parent) get :show, :id => p - assert_tag :h1, :parent => { :attributes => {:id => 'header'}}, - :children => { :count => [i, 3].min, - :only => { :tag => 'a' } } + assert_select '#header h1' do + assert_select 'a', :count => [i, 3].min + end parent = p end @@ -519,8 +544,7 @@ 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'} + assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1 end def test_get_copy_with_invalid_source_should_respond_with_404 @@ -575,4 +599,9 @@ assert_response :success assert_template 'show' end + + def test_body_should_have_project_css_class + get :show, :id => 1 + assert_select 'body.project-ecookbook' + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/queries_controller_test.rb --- a/test/functional/queries_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/queries_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,14 +24,18 @@ User.current = nil end + def test_index + get :index + # HTML response not implemented + assert_response 406 + end + def test_new_project_query @request.session[:user_id] = 2 get :new, :project_id => 1 assert_response :success assert_template 'new' - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'query[is_public]', - :checked => nil } + assert_select 'input[name=?][value=0][checked=checked]', 'query[visibility]' assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'query_is_for_all', :checked => nil, @@ -47,8 +51,7 @@ get :new assert_response :success assert_template 'new' - assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'query[is_public]' } + assert_select 'input[name=?]', 'query[visibility]', 0 assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'query_is_for_all', :checked => 'checked', @@ -69,7 +72,7 @@ :f => ["status_id", "assigned_to_id"], :op => {"assigned_to_id" => "=", "status_id" => "o"}, :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, - :query => {"name" => "test_new_project_public_query", "is_public" => "1"} + :query => {"name" => "test_new_project_public_query", "visibility" => "2"} q = Query.find_by_name('test_new_project_public_query') assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q @@ -86,7 +89,7 @@ :fields => ["status_id", "assigned_to_id"], :operators => {"assigned_to_id" => "=", "status_id" => "o"}, :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, - :query => {"name" => "test_new_project_private_query", "is_public" => "1"} + :query => {"name" => "test_new_project_private_query", "visibility" => "2"} q = Query.find_by_name('test_new_project_private_query') assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q @@ -101,14 +104,14 @@ :fields => ["status_id", "assigned_to_id"], :operators => {"assigned_to_id" => "=", "status_id" => "o"}, :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, - :query => {"name" => "test_new_global_private_query", "is_public" => "1"}, + :query => {"name" => "test_new_global_private_query", "visibility" => "2"}, :c => ["", "tracker", "subject", "priority", "category"] q = Query.find_by_name('test_new_global_private_query') assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q assert !q.is_public? assert !q.has_default_columns? - assert_equal [:tracker, :subject, :priority, :category], q.columns.collect {|c| c.name} + assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name} assert q.valid? end @@ -134,7 +137,7 @@ :operators => {"status_id" => "o"}, :values => {"status_id" => ["1"]}, :query => {:name => "test_new_with_sort", - :is_public => "1", + :visibility => "2", :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}} query = Query.find_by_name("test_new_with_sort") @@ -152,14 +155,49 @@ assert_select 'input[name=?]', 'query[name]' end + def test_create_global_query_from_gantt + @request.session[:user_id] = 1 + assert_difference 'IssueQuery.count' do + post :create, + :gantt => 1, + :operators => {"status_id" => "o"}, + :values => {"status_id" => ["1"]}, + :query => {:name => "test_create_from_gantt", + :draw_relations => '1', + :draw_progress_line => '1'} + assert_response 302 + end + query = IssueQuery.order('id DESC').first + assert_redirected_to "/issues/gantt?query_id=#{query.id}" + assert_equal true, query.draw_relations + assert_equal true, query.draw_progress_line + end + + def test_create_project_query_from_gantt + @request.session[:user_id] = 1 + assert_difference 'IssueQuery.count' do + post :create, + :project_id => 'ecookbook', + :gantt => 1, + :operators => {"status_id" => "o"}, + :values => {"status_id" => ["1"]}, + :query => {:name => "test_create_from_gantt", + :draw_relations => '0', + :draw_progress_line => '0'} + assert_response 302 + end + query = IssueQuery.order('id DESC').first + assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}" + assert_equal false, query.draw_relations + assert_equal false, query.draw_progress_line + end + def test_edit_global_public_query @request.session[:user_id] = 1 get :edit, :id => 4 assert_response :success assert_template 'edit' - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'query[is_public]', - :checked => 'checked' } + assert_select 'input[name=?][value=2][checked=checked]', 'query[visibility]' assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'query_is_for_all', :checked => 'checked', @@ -171,8 +209,7 @@ get :edit, :id => 3 assert_response :success assert_template 'edit' - assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'query[is_public]' } + assert_select 'input[name=?]', 'query[visibility]', 0 assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'query_is_for_all', :checked => 'checked', @@ -184,8 +221,7 @@ get :edit, :id => 2 assert_response :success assert_template 'edit' - assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'query[is_public]' } + assert_select 'input[name=?]', 'query[visibility]', 0 assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'query_is_for_all', :checked => nil, @@ -197,10 +233,7 @@ get :edit, :id => 1 assert_response :success assert_template 'edit' - assert_tag :tag => 'input', :attributes => { :type => 'checkbox', - :name => 'query[is_public]', - :checked => 'checked' - } + assert_select 'input[name=?][value=2][checked=checked]', 'query[visibility]' assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'query_is_for_all', :checked => nil, @@ -234,7 +267,7 @@ :fields => ["status_id", "assigned_to_id"], :operators => {"assigned_to_id" => "=", "status_id" => "o"}, :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, - :query => {"name" => "test_edit_global_private_query", "is_public" => "1"} + :query => {"name" => "test_edit_global_private_query", "visibility" => "2"} assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3 q = Query.find_by_name('test_edit_global_private_query') @@ -251,7 +284,7 @@ :fields => ["status_id", "assigned_to_id"], :operators => {"assigned_to_id" => "=", "status_id" => "o"}, :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, - :query => {"name" => "test_edit_global_public_query", "is_public" => "1"} + :query => {"name" => "test_edit_global_public_query", "visibility" => "2"} assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4 q = Query.find_by_name('test_edit_global_public_query') diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/reports_controller_test.rb --- a/test/functional/reports_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/reports_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,12 +25,8 @@ :member_roles, :members, :enabled_modules, - :workflows, :versions - def setup - end - def test_get_issue_report get :issue_report, :id => 1 @@ -58,6 +54,24 @@ end end + def test_get_issue_report_details_by_tracker_should_show_issue_count + Issue.delete_all + Issue.generate!(:tracker_id => 1) + Issue.generate!(:tracker_id => 1) + Issue.generate!(:tracker_id => 1, :status_id => 5) + Issue.generate!(:tracker_id => 2) + + get :issue_report_details, :id => 1, :detail => 'tracker' + assert_select 'table.list tbody :nth-child(1)' do + assert_select 'td', :text => 'Bug' + assert_select ':nth-child(2)', :text => '2' # status:1 + assert_select ':nth-child(3)', :text => '-' # status:2 + assert_select ':nth-child(8)', :text => '2' # open + assert_select ':nth-child(9)', :text => '1' # closed + assert_select ':nth-child(10)', :text => '3' # total + end + end + def test_get_issue_report_details_by_priority get :issue_report_details, :id => 1, :detail => 'priority' assert_equal IssuePriority.all.reverse, assigns(:rows) diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_bazaar_controller_test.rb --- a/test/functional/repositories_bazaar_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_bazaar_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -23,17 +23,23 @@ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules - REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository/trunk').to_s + REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s + REPOSITORY_PATH_TRUNK = File.join(REPOSITORY_PATH, "trunk") PRJ_ID = 3 + CHAR_1_UTF8_HEX = "\xc3\x9c" def setup User.current = nil @project = Project.find(PRJ_ID) @repository = Repository::Bazaar.create( :project => @project, - :url => REPOSITORY_PATH, + :url => REPOSITORY_PATH_TRUNK, :log_encoding => 'UTF-8') assert @repository + @char_1_utf8 = CHAR_1_UTF8_HEX.dup + if @char_1_utf8.respond_to?(:force_encoding) + @char_1_utf8.force_encoding('UTF-8') + end end if File.directory?(REPOSITORY_PATH) @@ -137,26 +143,68 @@ :path => repository_path_hash(['doc-mkdir.txt'])[:param] assert_response :success assert_template 'annotate' - assert_tag :tag => 'th', :content => '2', - :sibling => { - :tag => 'td', - :child => { - :tag => 'a', - :content => '3' - } - } - assert_tag :tag => 'th', :content => '2', - :sibling => { :tag => 'td', :content => /jsmith/ } - assert_tag :tag => 'th', :content => '2', - :sibling => { - :tag => 'td', - :child => { - :tag => 'a', - :content => '3' - } - } - assert_tag :tag => 'th', :content => '2', - :sibling => { :tag => 'td', :content => /Main purpose/ } + assert_select "th.line-num", :text => '2' do + assert_select "+ td.revision" do + assert_select "a", :text => '3' + assert_select "+ td.author", :text => "jsmith@" do + assert_select "+ td", + :text => "Main purpose:" + end + end + end + end + + def test_annotate_author_escaping + repository = Repository::Bazaar.create( + :project => @project, + :url => File.join(REPOSITORY_PATH, "author_escaping"), + :identifier => 'author_escaping', + :log_encoding => 'UTF-8') + assert repository + get :annotate, :id => PRJ_ID, :repository_id => 'author_escaping', + :path => repository_path_hash(['author-escaping-test.txt'])[:param] + assert_response :success + assert_template 'annotate' + assert_select "th.line-num", :text => '1' do + assert_select "+ td.revision" do + assert_select "a", :text => '2' + assert_select "+ td.author", :text => "test &" do + assert_select "+ td", + :text => "author escaping test" + end + end + end + end + + if REPOSITORY_PATH.respond_to?(:force_encoding) + def test_annotate_author_non_ascii + log_encoding = nil + if Encoding.locale_charmap == "UTF-8" || + Encoding.locale_charmap == "ISO-8859-1" + log_encoding = Encoding.locale_charmap + end + unless log_encoding.nil? + repository = Repository::Bazaar.create( + :project => @project, + :url => File.join(REPOSITORY_PATH, "author_non_ascii"), + :identifier => 'author_non_ascii', + :log_encoding => log_encoding) + assert repository + get :annotate, :id => PRJ_ID, :repository_id => 'author_non_ascii', + :path => repository_path_hash(['author-non-ascii-test.txt'])[:param] + assert_response :success + assert_template 'annotate' + assert_select "th.line-num", :text => '1' do + assert_select "+ td.revision" do + assert_select "a", :text => '2' + assert_select "+ td.author", :text => "test #{@char_1_utf8}" do + assert_select "+ td", + :text => "author non ASCII test" + end + end + end + end + end end def test_destroy_valid_repository diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_controller_test.rb --- a/test/functional/repositories_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,10 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'repositories_controller' - -# Re-raise errors caught by the controller. -class RepositoriesController; def rescue_action(e) raise e end; end class RepositoriesControllerTest < ActionController::TestCase fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, @@ -27,9 +23,6 @@ :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers def setup - @controller = RepositoriesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end @@ -118,6 +111,31 @@ assert_nil Repository.find_by_id(11) end + def test_show_with_autofetch_changesets_enabled_should_fetch_changesets + Repository::Subversion.any_instance.expects(:fetch_changesets).once + + with_settings :autofetch_changesets => '1' do + get :show, :id => 1 + end + end + + def test_show_with_autofetch_changesets_disabled_should_not_fetch_changesets + Repository::Subversion.any_instance.expects(:fetch_changesets).never + + with_settings :autofetch_changesets => '0' do + get :show, :id => 1 + end + end + + def test_show_with_closed_project_should_not_fetch_changesets + Repository::Subversion.any_instance.expects(:fetch_changesets).never + Project.find(1).close + + with_settings :autofetch_changesets => '1' do + get :show, :id => 1 + end + end + def test_revisions get :revisions, :id => 1 assert_response :success @@ -181,6 +199,14 @@ assert_include 'Feature request #2', response.body end + def test_add_related_issue_should_accept_issue_id_with_sharp + @request.session[:user_id] = 2 + assert_difference 'Changeset.find(103).issues.size' do + xhr :post, :add_related_issue, :id => 1, :rev => 4, :issue_id => "#2", :format => 'js' + end + assert_equal [2], Changeset.find(103).issue_ids + end + def test_add_related_issue_with_invalid_issue_id @request.session[:user_id] = 2 assert_no_difference 'Changeset.find(103).issues.size' do @@ -262,7 +288,7 @@ :revision => 100, :comments => 'Committed by foo.' ) - assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do + assert_no_difference "Changeset.where(:user_id => 3).count" do post :committers, :id => 10, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']} assert_response 302 assert_equal User.find(2), c.reload.user diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_cvs_controller_test.rb --- a/test/functional/repositories_cvs_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_cvs_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_darcs_controller_test.rb --- a/test/functional/repositories_darcs_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_darcs_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_filesystem_controller_test.rb --- a/test/functional/repositories_filesystem_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_filesystem_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -109,7 +109,8 @@ end def test_show_utf16 - with_settings :repositories_encodings => 'UTF-16' do + enc = (RUBY_VERSION == "1.9.2" ? 'UTF-16LE' : 'UTF-16') + with_settings :repositories_encodings => enc do get :entry, :id => PRJ_ID, :path => repository_path_hash(['japanese', 'utf-16.txt'])[:param] assert_response :success diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_git_controller_test.rb --- a/test/functional/repositories_git_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_git_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -27,6 +27,7 @@ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? PRJ_ID = 3 CHAR_1_HEX = "\xc3\x9c" + FELIX_HEX = "Felix Sch\xC3\xA4fer" NUM_REV = 28 ## Git, Mercurial and CVS path encodings are binary. @@ -50,8 +51,10 @@ ) assert @repository @char_1 = CHAR_1_HEX.dup + @felix_utf8 = FELIX_HEX.dup if @char_1.respond_to?(:force_encoding) @char_1.force_encoding('UTF-8') + @felix_utf8.force_encoding('UTF-8') end end @@ -532,11 +535,32 @@ 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/ } + assert_select "th.line-num", :text => '1' do + assert_select "+ td.revision" do + assert_select "a", :text => '57ca437c' + assert_select "+ td.author", :text => "jsmith" do + assert_select "+ td", + :text => "test-#{@char_1}.txt" + end + end + end + end + end + end + end + + def test_annotate_latin_1_author + ['83ca5fd546063a3c7dc2e568ba3355661a9e2b2c', '83ca5fd546063a'].each do |r1| + get :annotate, :id => PRJ_ID, + :path => repository_path_hash([" filename with a leading space.txt "])[:param], + :rev => r1 + assert_select "th.line-num", :text => '1' do + assert_select "+ td.revision" do + assert_select "a", :text => '83ca5fd5' + assert_select "+ td.author", :text => @felix_utf8 do + assert_select "+ td", + :text => "And this is a file with a leading and trailing space..." + end end end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_mercurial_controller_test.rb --- a/test/functional/repositories_mercurial_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_mercurial_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -432,30 +432,15 @@ :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/ } - + assert_select "th.line-num", :text => '1' do + assert_select "+ td.revision" do + assert_select "a", :text => '20:709858aafd1b' + assert_select "+ td.author", :text => "jsmith" do + assert_select "+ td", + :text => "Mercurial is a distributed version control system." + end + end + end end end @@ -474,6 +459,22 @@ end end + def test_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['1', '9d5b5b', '9d5b5b004199'].each do |r| + with_settings :default_language => "en" do + get :revision, :id => PRJ_ID, :rev => r + assert_response :success + assert_template 'revision' + assert_select 'title', + :text => 'Revision 1:9d5b5b004199 - Added 2 files and modified one. - eCookbook Subproject 1 - Redmine' + end + end + end + def test_empty_revision assert_equal 0, @repository.changesets.count @repository.fetch_changesets diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/repositories_subversion_controller_test.rb --- a/test/functional/repositories_subversion_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/repositories_subversion_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -326,10 +326,10 @@ @project.reload assert_equal NUM_REV, @repository.changesets.count - get :diff, :id => PRJ_ID, :rev => 3, :format => 'diff' + get :diff, :id => PRJ_ID, :rev => 5, :format => 'diff' assert_response :success assert_equal 'text/x-patch', @response.content_type - assert_equal 'Index: subversion_test/textfile.txt', @response.body.split(/\r?\n/).first + assert_equal 'Index: subversion_test/folder/greeter.rb', @response.body.split(/\r?\n/).first end def test_directory_diff diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/roles_controller_test.rb --- a/test/functional/roles_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/roles_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,9 +21,6 @@ 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 @@ -34,7 +31,7 @@ assert_template 'index' assert_not_nil assigns(:roles) - assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles) + assert_equal Role.order('builtin, position').all, assigns(:roles) assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' }, :content => 'Manager' @@ -163,7 +160,7 @@ assert_template 'permissions' assert_not_nil assigns(:roles) - assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles) + assert_equal Role.order('builtin, position').all, assigns(:roles) assert_tag :tag => 'input', :attributes => { :type => 'checkbox', :name => 'permissions[3][]', diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/search_controller_test.rb --- a/test/functional/search_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/search_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,8 +1,21 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) -require 'search_controller' - -# Re-raise errors caught by the controller. -class SearchController; def rescue_action(e) raise e end; end class SearchControllerTest < ActionController::TestCase fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles, @@ -11,9 +24,6 @@ :repositories, :changesets def setup - @controller = SearchController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/search_custom_fields_visibility_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/functional/search_custom_fields_visibility_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,78 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class SearchCustomFieldsVisibilityTest < ActionController::TestCase + tests SearchController + 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, :searchable => 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_search_should_search_visible_custom_fields_only + @users_to_test.each do |user, fields| + @request.session[:user_id] = user.id + @fields.each_with_index do |field, i| + get :index, :q => "value#{i}" + assert_response :success + # we should get a result only if the custom field is visible + if fields.include?(field) + assert_equal 1, assigns(:results).size + else + assert_equal 0, assigns(:results).size + end + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/sessions_test.rb --- a/test/functional/sessions_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/sessions_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/settings_controller_test.rb --- a/test/functional/settings_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/settings_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'settings_controller' - -# Re-raise errors caught by the controller. -class SettingsController; def rescue_action(e) raise e end; end class SettingsControllerTest < ActionController::TestCase fixtures :users def setup - @controller = SettingsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil @request.session[:user_id] = 1 # admin end @@ -79,7 +72,7 @@ :notified_events => %w(issue_added issue_updated news_added), :emails_footer => 'Test footer' } - assert_redirected_to '/settings/edit' + assert_redirected_to '/settings' assert_equal 'functional@test.foo', Setting.mail_from assert !Setting.bcc_recipients? assert_equal %w(issue_added issue_updated news_added), Setting.notified_events @@ -87,6 +80,61 @@ Setting.clear_cache end + def test_edit_commit_update_keywords + with_settings :commit_update_keywords => [ + {"keywords" => "fixes, resolves", "status_id" => "3"}, + {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"} + ] do + get :edit + end + assert_response :success + assert_select 'tr.commit-keywords', 2 + assert_select 'tr.commit-keywords:nth-child(1)' do + assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves' + assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do + assert_select 'option[value=3][selected=selected]' + end + end + assert_select 'tr.commit-keywords:nth-child(2)' do + assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes' + assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do + assert_select 'option[value=5][selected=selected]', :text => 'Closed' + end + assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do + assert_select 'option[value=100][selected=selected]', :text => '100 %' + end + assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do + assert_select 'option[value=2][selected=selected]', :text => 'Feature request' + end + end + end + + def test_edit_without_commit_update_keywords_should_show_blank_line + with_settings :commit_update_keywords => [] do + get :edit + end + assert_response :success + assert_select 'tr.commit-keywords', 1 do + assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]' + end + end + + def test_post_edit_commit_update_keywords + post :edit, :settings => { + :commit_update_keywords => { + :keywords => ["resolves", "closes"], + :status_id => ["3", "5"], + :done_ratio => ["", "100"], + :if_tracker_id => ["", "2"] + } + } + assert_redirected_to '/settings' + assert_equal([ + {"keywords" => "resolves", "status_id" => "3"}, + {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"} + ], Setting.commit_update_keywords) + end + def test_get_plugin_settings Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'}) ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins")) @@ -108,11 +156,31 @@ assert_response 404 end + def test_get_non_configurable_plugin_settings + Redmine::Plugin.register(:foo) {} + + get :plugin, :id => 'foo' + assert_response 404 + + Redmine::Plugin.clear + end + def test_post_plugin_settings Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true) - Redmine::Plugin.register(:foo) {} + Redmine::Plugin.register(:foo) do + settings :partial => 'not blank' # so that configurable? is true + end post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'} assert_redirected_to '/settings/plugin/foo' end + + def test_post_non_configurable_plugin_settings + Redmine::Plugin.register(:foo) {} + + post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'} + assert_response 404 + + Redmine::Plugin.clear + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/sys_controller_test.rb --- a/test/functional/sys_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/sys_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,19 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'sys_controller' -require 'mocha' - -# Re-raise errors caught by the controller. -class SysController; def rescue_action(e) raise e end; end class SysControllerTest < ActionController::TestCase fixtures :projects, :repositories, :enabled_modules def setup - @controller = SysController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new Setting.sys_api_enabled = '1' Setting.enabled_scm = %w(Subversion Git) end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/time_entry_reports_controller_test.rb --- a/test/functional/time_entry_reports_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/time_entry_reports_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,4 +1,21 @@ # -*- coding: utf-8 -*- +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../test_helper', __FILE__) class TimeEntryReportsControllerTest < ActionController::TestCase @@ -73,7 +90,7 @@ end def test_report_two_criteria - get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criteria => ["member", "activity"] + 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) @@ -91,7 +108,7 @@ end def test_report_one_day - get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criteria => ["member", "activity"] + 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) @@ -99,7 +116,7 @@ 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 => ["member", "activity"] + 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) @@ -108,22 +125,62 @@ :attributes => {:action => "/projects/ecookbook/issues/1/time_entries/report", :id => 'query_form'} end - def test_report_custom_field_criteria - get :report, :project_id => 1, :criteria => ['project', 'cf_1', 'cf_7'] + 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 field column - assert_tag :tag => 'th', :content => 'Database' + + # Custom fields columns + assert_select 'th', :text => 'Database' + assert_select 'th', :text => 'Development status' + assert_select 'th', :text => 'Billable' + # Custom field row - assert_tag :tag => 'td', :content => 'MySQL', - :sibling => { :tag => 'td', :attributes => { :class => 'hours' }, - :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' }, - :content => '1' }} - # Second custom field column - assert_tag :tag => 'th', :content => 'Billable' + 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 @@ -144,29 +201,27 @@ def test_report_all_projects_csv_export get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", - :criteria => ["project", "member", "activity"], :format => "csv" + :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,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', - lines.first + assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first # Total row - assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last + 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", "member", "activity"], :format => "csv" + :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,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', - lines.first + assert_equal 'Project,User,Activity,2007-3,2007-4,Total time', lines.first # Total row - assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last + assert_equal 'Total time,"","",154.25,8.65,162.90', lines.last end def test_csv_big_5 @@ -196,13 +251,13 @@ get :report, :project_id => 1, :columns => 'day', :from => "2011-11-11", :to => "2011-11-11", - :criteria => ["member"], :format => "csv" + :criteria => ["user"], :format => "csv" assert_response :success assert_equal 'text/csv; header=present', @response.content_type lines = @response.body.chomp.split("\n") # Headers - s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp" - s2 = "\xc1`\xadp" + 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') @@ -247,12 +302,12 @@ get :report, :project_id => 1, :columns => 'day', :from => "2011-11-11", :to => "2011-11-11", - :criteria => ["member"], :format => "csv" + :criteria => ["user"], :format => "csv" assert_response :success assert_equal 'text/csv; header=present', @response.content_type lines = @response.body.chomp.split("\n") # Headers - s1 = "\xa6\xa8\xad\xfb,2011-11-11,\xc1`\xadp" + s1 = "\xa5\xce\xa4\xe1,2011-11-11,\xa4u\xae\xc9\xc1`\xadp" if s1.respond_to?(:force_encoding) s1.force_encoding('Big5') end @@ -288,13 +343,13 @@ get :report, :project_id => 1, :columns => 'day', :from => "2011-11-11", :to => "2011-11-11", - :criteria => ["member"], :format => "csv" + :criteria => ["user"], :format => "csv" assert_response :success assert_equal 'text/csv; header=present', @response.content_type lines = @response.body.chomp.split("\n") # Headers - s1 = "Membre;2011-11-11;Total" - s2 = "Total" + 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') diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/timelog_controller_test.rb --- a/test/functional/timelog_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/timelog_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,25 +17,17 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'timelog_controller' - -# Re-raise errors caught by the controller. -class TimelogController; def rescue_action(e) raise e end; end class TimelogControllerTest < ActionController::TestCase fixtures :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, - :custom_fields, :custom_values + :custom_fields, :custom_values, + :projects_trackers, :custom_fields_trackers, + :custom_fields_projects include Redmine::I18n - def setup - @controller = TimelogController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - def test_new_with_project_id @request.session[:user_id] = 3 get :new, :project_id => 1 @@ -303,13 +295,20 @@ assert_response :success assert_template 'bulk_edit' - # System wide custom field - assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'} + assert_select 'ul#bulk-selection' do + assert_select 'li', 2 + assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours' + end - # Activities - assert_select 'select[name=?]', 'time_entry[activity_id]' do - assert_select 'option[value=]', :text => '(No change)' - assert_select 'option[value=9]', :text => 'Design' + assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do + # System wide custom field + assert_select 'select[name=?]', 'time_entry[custom_field_values][10]' + + # Activities + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[value=]', :text => '(No change)' + assert_select 'option[value=9]', :text => 'Design' + end end end @@ -430,6 +429,14 @@ assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/ end + def test_index_my_spent_time + @request.session[:user_id] = 2 + get :index, :user_id => 'me' + assert_response :success + assert_template 'index' + assert assigns(:entries).all? {|entry| entry.user_id == 2} + end + def test_index_at_project_level get :index, :project_id => 'ecookbook' assert_response :success @@ -440,14 +447,48 @@ assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort assert_not_nil assigns(:total_hours) assert_equal "162.90", "%.2f" % assigns(:total_hours) - # display all time by default - assert_nil assigns(:from) - assert_nil assigns(:to) assert_tag :form, :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} end + def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries + entry = TimeEntry.generate!(:project => Project.find(3)) + + with_settings :display_subprojects_issues => '0' do + get :index, :project_id => 'ecookbook' + assert_response :success + assert_template 'index' + assert_not_include entry, assigns(:entries) + end + end + + def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries + entry = TimeEntry.generate!(:project => Project.find(3)) + + with_settings :display_subprojects_issues => '0' do + get :index, :project_id => 'ecookbook', :subproject_id => 3 + assert_response :success + assert_template 'index' + assert_include entry, assigns(:entries) + end + end + def test_index_at_project_level_with_date_range + get :index, :project_id => 'ecookbook', + :f => ['spent_on'], + :op => {'spent_on' => '><'}, + :v => {'spent_on' => ['2007-03-20', '2007-04-30']} + assert_response :success + assert_template 'index' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + assert_not_nil assigns(:total_hours) + assert_equal "12.90", "%.2f" % assigns(:total_hours) + assert_tag :form, + :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} + end + + def test_index_at_project_level_with_date_range_using_from_and_to_params get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30' assert_response :success assert_template 'index' @@ -455,116 +496,23 @@ assert_equal 3, assigns(:entries).size assert_not_nil assigns(:total_hours) assert_equal "12.90", "%.2f" % assigns(:total_hours) - assert_equal '2007-03-20'.to_date, assigns(:from) - assert_equal '2007-04-30'.to_date, assigns(:to) assert_tag :form, :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} end def test_index_at_project_level_with_period - get :index, :project_id => 'ecookbook', :period => '7_days' + get :index, :project_id => 'ecookbook', + :f => ['spent_on'], + :op => {'spent_on' => '>t-'}, + :v => {'spent_on' => ['7']} assert_response :success assert_template 'index' assert_not_nil assigns(:entries) assert_not_nil assigns(:total_hours) - assert_equal Date.today - 7, assigns(:from) - assert_equal Date.today, assigns(:to) assert_tag :form, :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} end - def test_index_one_day - get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "2007-03-23" - assert_response :success - assert_template 'index' - assert_not_nil assigns(:total_hours) - assert_equal "4.25", "%.2f" % assigns(:total_hours) - assert_tag :form, - :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'} - end - - def test_index_from_a_date - get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "" - assert_equal '2007-03-23'.to_date, assigns(:from) - assert_nil assigns(:to) - end - - def test_index_to_a_date - get :index, :project_id => 'ecookbook', :from => "", :to => "2007-03-23" - assert_nil assigns(:from) - assert_equal '2007-03-23'.to_date, assigns(:to) - end - - def test_index_today - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'today' - assert_equal '2011-12-15'.to_date, assigns(:from) - assert_equal '2011-12-15'.to_date, assigns(:to) - end - - def test_index_yesterday - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'yesterday' - assert_equal '2011-12-14'.to_date, assigns(:from) - assert_equal '2011-12-14'.to_date, assigns(:to) - end - - def test_index_current_week - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'current_week' - assert_equal '2011-12-12'.to_date, assigns(:from) - assert_equal '2011-12-18'.to_date, assigns(:to) - end - - def test_index_last_week - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'last_week' - assert_equal '2011-12-05'.to_date, assigns(:from) - assert_equal '2011-12-11'.to_date, assigns(:to) - end - - def test_index_last_2_week - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'last_2_weeks' - assert_equal '2011-11-28'.to_date, assigns(:from) - assert_equal '2011-12-11'.to_date, assigns(:to) - end - - def test_index_7_days - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => '7_days' - assert_equal '2011-12-08'.to_date, assigns(:from) - assert_equal '2011-12-15'.to_date, assigns(:to) - end - - def test_index_current_month - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'current_month' - assert_equal '2011-12-01'.to_date, assigns(:from) - assert_equal '2011-12-31'.to_date, assigns(:to) - end - - def test_index_last_month - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'last_month' - assert_equal '2011-11-01'.to_date, assigns(:from) - assert_equal '2011-11-30'.to_date, assigns(:to) - end - - def test_index_30_days - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => '30_days' - assert_equal '2011-11-15'.to_date, assigns(:from) - assert_equal '2011-12-15'.to_date, assigns(:to) - end - - def test_index_current_year - Date.stubs(:today).returns('2011-12-15'.to_date) - get :index, :period => 'current_year' - assert_equal '2011-01-01'.to_date, assigns(:from) - assert_equal '2011-12-31'.to_date, assigns(:to) - end - def test_index_at_issue_level get :index, :issue_id => 1 assert_response :success @@ -587,15 +535,70 @@ t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10) t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10) - get :index, :project_id => 1, :from => '2012-06-15', :to => '2012-06-16' + get :index, :project_id => 1, + :f => ['spent_on'], + :op => {'spent_on' => '><'}, + :v => {'spent_on' => ['2012-06-15', '2012-06-16']} assert_response :success assert_equal [t2, t1, t3], assigns(:entries) - get :index, :project_id => 1, :from => '2012-06-15', :to => '2012-06-16', :sort => 'spent_on' + get :index, :project_id => 1, + :f => ['spent_on'], + :op => {'spent_on' => '><'}, + :v => {'spent_on' => ['2012-06-15', '2012-06-16']}, + :sort => 'spent_on' assert_response :success assert_equal [t3, t1, t2], assigns(:entries) end + def test_index_with_filter_on_issue_custom_field + issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'}) + entry = TimeEntry.generate!(:issue => issue, :hours => 2.5) + + get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']} + assert_response :success + assert_equal [entry], assigns(:entries) + end + + def test_index_with_issue_custom_field_column + issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'}) + entry = TimeEntry.generate!(:issue => issue, :hours => 2.5) + + get :index, :c => %w(project spent_on issue comments hours issue.cf_2) + assert_response :success + assert_include :'issue.cf_2', assigns(:query).column_names + assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field' + end + + def test_index_with_time_entry_custom_field_column + field = TimeEntryCustomField.generate!(:field_format => 'string') + entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'}) + field_name = "cf_#{field.id}" + + get :index, :c => ["hours", field_name] + assert_response :success + assert_include field_name.to_sym, assigns(:query).column_names + assert_select "td.#{field_name}", :text => 'CF Value' + end + + def test_index_with_time_entry_custom_field_sorting + field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field') + TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'}) + TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'}) + TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'}) + field_name = "cf_#{field.id}" + + get :index, :c => ["hours", field_name], :sort => field_name + assert_response :success + assert_include field_name.to_sym, assigns(:query).column_names + assert_select "th a.sort", :text => 'String Field' + + # Make sure that values are properly sorted + values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact + assert_equal 3, values.size + assert_equal values.sort, values + end + def test_index_atom_feed get :index, :project_id => 1, :format => 'atom' assert_response :success @@ -604,186 +607,57 @@ assert assigns(:items).first.is_a?(TimeEntry) end - def test_index_all_projects_csv_export + def test_index_at_project_level_should_include_csv_export_dialog + get :index, :project_id => 'ecookbook', + :f => ['spent_on'], + :op => {'spent_on' => '>='}, + :v => {'spent_on' => ['2007-04-01']}, + :c => ['spent_on', 'user'] + assert_response :success + + assert_select '#csv-export-options' do + assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do + # filter + assert_select 'input[name=?][value=?]', 'f[]', 'spent_on' + assert_select 'input[name=?][value=?]', 'op[spent_on]', '>=' + assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01' + # columns + assert_select 'input[name=?][value=?]', 'c[]', 'spent_on' + assert_select 'input[name=?][value=?]', 'c[]', 'user' + assert_select 'input[name=?]', 'c[]', 2 + end + end + end + + def test_index_cross_project_should_include_csv_export_dialog + get :index + assert_response :success + + assert_select '#csv-export-options' do + assert_select 'form[action=?][method=get]', '/time_entries.csv' + end + end + + def test_index_at_issue_level_should_include_csv_export_dialog + get :index, :project_id => 'ecookbook', :issue_id => 3 + assert_response :success + + assert_select '#csv-export-options' do + assert_select 'form[action=?][method=get]', '/projects/ecookbook/issues/3/time_entries.csv' + end + end + + def test_index_csv_all_projects Setting.date_format = '%m/%d/%Y' get :index, :format => 'csv' assert_response :success - assert_equal 'text/csv; header=present', @response.content_type - assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n") - assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n") + assert_equal 'text/csv; header=present', response.content_type end - def test_index_csv_export + def test_index_csv Setting.date_format = '%m/%d/%Y' get :index, :project_id => 1, :format => 'csv' assert_response :success - assert_equal 'text/csv; header=present', @response.content_type - assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n") - assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n") - end - - def test_index_csv_export_with_multi_custom_field - field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'list', - :multiple => true, :possible_values => ['value1', 'value2']) - entry = TimeEntry.find(1) - entry.custom_field_values = {field.id => ['value1', 'value2']} - entry.save! - - get :index, :project_id => 1, :format => 'csv' - assert_response :success - assert_include '"value1, value2"', @response.body - end - - def test_csv_big_5 - user = User.find_by_id(3) - user.language = "zh-TW" - assert user.save - 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 - @request.session[:user_id] = 3 - post :create, :project_id => 1, - :time_entry => {:comments => str_utf8, - # Not the default activity - :activity_id => '11', - :issue_id => '', - :spent_on => '2011-11-10', - :hours => '7.3'} - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - - t = TimeEntry.find_by_comments(str_utf8) - assert_not_nil t - assert_equal 11, t.activity_id - assert_equal 7.3, t.hours - assert_equal 3, t.user_id - - get :index, :project_id => 1, :format => 'csv', - :from => '2011-11-10', :to => '2011-11-10' - assert_response :success - assert_equal 'text/csv; header=present', @response.content_type - ar = @response.body.chomp.split("\n") - s1 = "\xa4\xe9\xb4\xc1" - if str_utf8.respond_to?(:force_encoding) - s1.force_encoding('Big5') - end - assert ar[0].include?(s1) - assert ar[1].include?(str_big5) - end - - def test_csv_cannot_convert_should_be_replaced_big_5 - user = User.find_by_id(3) - user.language = "zh-TW" - assert user.save - str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85" - if str_utf8.respond_to?(:force_encoding) - str_utf8.force_encoding('UTF-8') - end - @request.session[:user_id] = 3 - post :create, :project_id => 1, - :time_entry => {:comments => str_utf8, - # Not the default activity - :activity_id => '11', - :issue_id => '', - :spent_on => '2011-11-10', - :hours => '7.3'} - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - - t = TimeEntry.find_by_comments(str_utf8) - assert_not_nil t - assert_equal 11, t.activity_id - assert_equal 7.3, t.hours - assert_equal 3, t.user_id - - get :index, :project_id => 1, :format => 'csv', - :from => '2011-11-10', :to => '2011-11-10' - assert_response :success - assert_equal 'text/csv; header=present', @response.content_type - ar = @response.body.chomp.split("\n") - s1 = "\xa4\xe9\xb4\xc1" - if str_utf8.respond_to?(:force_encoding) - s1.force_encoding('Big5') - end - assert ar[0].include?(s1) - s2 = ar[1].split(",")[8] - if s2.respond_to?(:force_encoding) - s3 = "\xa5H?" - s3.force_encoding('Big5') - assert_equal s3, s2 - elsif RUBY_PLATFORM == 'java' - assert_equal "??", s2 - else - assert_equal "\xa5H???", s2 - end - end - - def test_csv_tw - with_settings :default_language => "zh-TW" do - str1 = "test_csv_tw" - user = User.find_by_id(3) - te1 = TimeEntry.create(:spent_on => '2011-11-10', - :hours => 999.9, - :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 999.9, te2.hours - assert_equal 3, te2.user_id - - get :index, :project_id => 1, :format => 'csv', - :from => '2011-11-10', :to => '2011-11-10' - assert_response :success - assert_equal 'text/csv; header=present', @response.content_type - - ar = @response.body.chomp.split("\n") - s2 = ar[1].split(",")[7] - assert_equal '999.9', s2 - - 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 ',', l(:general_csv_separator) - assert_equal '.', l(:general_csv_decimal_separator) - end - 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-10', - :hours => 999.9, - :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 999.9, te2.hours - assert_equal 3, te2.user_id - - get :index, :project_id => 1, :format => 'csv', - :from => '2011-11-10', :to => '2011-11-10' - assert_response :success - assert_equal 'text/csv; header=present', @response.content_type - - ar = @response.body.chomp.split("\n") - s2 = ar[1].split(";")[7] - assert_equal '999,9', s2 - - 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 ';', l(:general_csv_separator) - assert_equal ',', l(:general_csv_decimal_separator) - end + assert_equal 'text/csv; header=present', response.content_type end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/timelog_custom_fields_visibility_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/functional/timelog_custom_fields_visibility_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,113 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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 038ba2d95de8 -r 261b3d9a4903 test/functional/trackers_controller_test.rb --- a/test/functional/trackers_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/trackers_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'trackers_controller' - -# Re-raise errors caught by the controller. -class TrackersController; def rescue_action(e) raise e end; end class TrackersControllerTest < ActionController::TestCase fixtures :trackers, :projects, :projects_trackers, :users, :issues, :custom_fields def setup - @controller = TrackersController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil @request.session[:user_id] = 1 # admin end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/users_controller_test.rb --- a/test/functional/users_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/users_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,10 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'users_controller' - -# Re-raise errors caught by the controller. -class UsersController; def rescue_action(e) raise e end; end class UsersControllerTest < ActionController::TestCase include Redmine::I18n @@ -29,9 +25,6 @@ :auth_sources def setup - @controller = UsersController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil @request.session[:user_id] = 1 # admin end @@ -40,12 +33,6 @@ get :index assert_response :success assert_template 'index' - end - - def test_index - get :index - assert_response :success - assert_template 'index' assert_not_nil assigns(:users) # active users only assert_nil assigns(:users).detect {|u| !u.active?} @@ -225,6 +212,30 @@ assert_equal '0', user.pref[:warn_on_leaving_unsaved] end + def test_create_with_generate_password_should_email_the_password + assert_difference 'User.count' do + post :create, :user => { + :login => 'randompass', + :firstname => 'Random', + :lastname => 'Pass', + :mail => 'randompass@example.net', + :language => 'en', + :generate_password => '1', + :password => '', + :password_confirmation => '' + }, :send_information => 1 + end + user = User.order('id DESC').first + assert_equal 'randompass', user.login + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/) + assert m + password = m[1] + assert user.check_password?(password) + end + def test_create_with_failure assert_no_difference 'User.count' do post :create, :user => {} @@ -297,6 +308,37 @@ assert_mail_body_match 'newpass123', mail end + def test_update_with_generate_password_should_email_the_password + ActionMailer::Base.deliveries.clear + Setting.bcc_recipients = '1' + + put :update, :id => 2, :user => { + :generate_password => '1', + :password => '', + :password_confirmation => '' + }, :send_information => '1' + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/) + assert m + password = m[1] + assert User.find(2).check_password?(password) + end + + def test_update_without_generate_password_should_not_change_password + put :update, :id => 2, :user => { + :firstname => 'changed', + :generate_password => '0', + :password => '', + :password_confirmation => '' + }, :send_information => '1' + + user = User.find(2) + assert_equal 'changed', user.firstname + assert user.check_password?('jsmith') + end + def test_update_user_switchin_from_auth_source_to_password_authentication # Configure as auth source u = User.find(2) @@ -316,22 +358,30 @@ u = User.find(2) assert_equal [1, 2, 5], u.projects.collect{|p| p.id}.sort assert_equal [1, 2, 5], u.notified_projects_ids.sort - assert_tag :tag => 'input', - :attributes => { - :id => 'notified_project_ids_', - :value => 1, - } + assert_select 'input[name=?][value=?]', 'user[notified_project_ids][]', '1' assert_equal 'all', u.mail_notification put :update, :id => 2, :user => { - :mail_notification => 'selected', - }, - :notified_project_ids => [1, 2] + :mail_notification => 'selected', + :notified_project_ids => [1, 2] + } u = User.find(2) assert_equal 'selected', u.mail_notification assert_equal [1, 2], u.notified_projects_ids.sort end + def test_update_status_should_not_update_attributes + user = User.find(2) + user.pref[:no_self_notified] = '1' + user.pref.save + + put :update, :id => 2, :user => {:status => 3} + assert_response 302 + user = User.find(2) + assert_equal 3, user.status + assert_equal '1', user.pref[:no_self_notified] + end + def test_destroy assert_difference 'User.count', -1 do delete :destroy, :id => 2 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/versions_controller_test.rb --- a/test/functional/versions_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/versions_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'versions_controller' - -# Re-raise errors caught by the controller. -class VersionsController; def rescue_action(e) raise e end; end class VersionsControllerTest < ActionController::TestCase - fixtures :projects, :versions, :issues, :users, :roles, :members, :member_roles, :enabled_modules, :issue_statuses, :issue_categories + fixtures :projects, :versions, :issues, :users, :roles, :members, + :member_roles, :enabled_modules, :issue_statuses, + :issue_categories def setup - @controller = VersionsController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end @@ -42,12 +37,12 @@ assert !assigns(:versions).include?(Version.find(1)) # Context menu on issues assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')")) - # Links to versions anchors - assert_tag 'a', :attributes => {:href => '#2.0'}, - :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}} - # Links to completed versions in the sidebar - assert_tag 'a', :attributes => {:href => '/versions/1'}, - :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}} + 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 @@ -178,16 +173,18 @@ 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_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 => {: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 @@ -207,7 +204,8 @@ assert_difference 'Version.count', -1 do delete :destroy, :id => 3 end - assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert_redirected_to :controller => 'projects', :action => 'settings', + :tab => 'versions', :id => 'ecookbook' assert_nil Version.find_by_id(3) end @@ -216,7 +214,8 @@ assert_no_difference 'Version.count' do delete :destroy, :id => 2 end - assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/watchers_controller_test.rb --- a/test/functional/watchers_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/watchers_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,23 +16,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'watchers_controller' - -# Re-raise errors caught by the controller. -class WatchersController; def rescue_action(e) raise e end; end class WatchersControllerTest < ActionController::TestCase fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers def setup - @controller = WatchersController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end - def test_watch + 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' @@ -42,6 +35,27 @@ 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 @@ -70,13 +84,27 @@ def test_unwatch @request.session[:user_id] = 3 assert_difference('Watcher.count', -1) do - xhr :post, :unwatch, :object_type => 'issue', :object_id => '2' + 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' @@ -84,7 +112,7 @@ assert_match /ajax-modal/, response.body end - def test_new_for_new_record_with_id + def test_new_for_new_record_with_project_id @request.session[:user_id] = 2 xhr :get, :new, :project_id => 1 assert_response :success @@ -92,7 +120,7 @@ assert_match /ajax-modal/, response.body end - def test_new_for_new_record_with_identifier + def test_new_for_new_record_with_project_identifier @request.session[:user_id] = 2 xhr :get, :new, :project_id => 'ecookbook' assert_response :success @@ -124,7 +152,8 @@ end def test_autocomplete_on_watchable_creation - xhr :get, :autocomplete_for_user, :q => 'mi' + @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][]' @@ -134,7 +163,8 @@ end def test_autocomplete_on_watchable_update - xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue' + @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][]' @@ -146,7 +176,7 @@ def test_append @request.session[:user_id] = 2 assert_no_difference 'Watcher.count' do - xhr :post, :append, :watcher => {:user_ids => ['4', '7']} + 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 @@ -156,7 +186,7 @@ def test_remove_watcher @request.session[:user_id] = 2 assert_difference('Watcher.count', -1) do - xhr :post, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3' + xhr :delete, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3' assert_response :success assert_match /watchers/, response.body end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/welcome_controller_test.rb --- a/test/functional/welcome_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/welcome_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'welcome_controller' - -# Re-raise errors caught by the controller. -class WelcomeController; def rescue_action(e) raise e end; end class WelcomeControllerTest < ActionController::TestCase fixtures :projects, :news, :users, :members def setup - @controller = WelcomeController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end @@ -37,7 +30,7 @@ assert_template 'index' assert_not_nil assigns(:news) assert_not_nil assigns(:projects) - assert !assigns(:projects).include?(Project.find(:first, :conditions => {:is_public => false})) + assert !assigns(:projects).include?(Project.where(:is_public => false).first) end def test_browser_language @@ -92,6 +85,13 @@ :content => %r{warnLeavingUnsaved} end + def test_logout_link_should_post + @request.session[:user_id] = 2 + + get :index + assert_select 'a[href=/logout][data-method=post]', :text => 'Sign out' + end + def test_call_hook_mixed_in assert @controller.respond_to?(:call_hook) end @@ -106,60 +106,50 @@ end end - context "test_api_offset_and_limit" do - context "without params" do - should "return 0, 25" do - assert_equal [0, 25], @controller.api_offset_and_limit({}) - end - end + def test_api_offset_and_limit_without_params + assert_equal [0, 25], @controller.api_offset_and_limit({}) + end - context "with limit" do - should "return 0, limit" do - assert_equal [0, 30], @controller.api_offset_and_limit({:limit => 30}) - end + def test_api_offset_and_limit_with_limit + assert_equal [0, 30], @controller.api_offset_and_limit({:limit => 30}) + assert_equal [0, 100], @controller.api_offset_and_limit({:limit => 120}) + assert_equal [0, 25], @controller.api_offset_and_limit({:limit => -10}) + end - should "not exceed 100" do - assert_equal [0, 100], @controller.api_offset_and_limit({:limit => 120}) - end + def test_api_offset_and_limit_with_offset + assert_equal [10, 25], @controller.api_offset_and_limit({:offset => 10}) + assert_equal [0, 25], @controller.api_offset_and_limit({:offset => -10}) + end - should "not be negative" do - assert_equal [0, 25], @controller.api_offset_and_limit({:limit => -10}) - end - end + def test_api_offset_and_limit_with_offset_and_limit + assert_equal [10, 50], @controller.api_offset_and_limit({:offset => 10, :limit => 50}) + end - context "with offset" do - should "return offset, 25" do - assert_equal [10, 25], @controller.api_offset_and_limit({:offset => 10}) - end + def test_api_offset_and_limit_with_page + assert_equal [0, 25], @controller.api_offset_and_limit({:page => 1}) + assert_equal [50, 25], @controller.api_offset_and_limit({:page => 3}) + assert_equal [0, 25], @controller.api_offset_and_limit({:page => 0}) + assert_equal [0, 25], @controller.api_offset_and_limit({:page => -2}) + end - should "not be negative" do - assert_equal [0, 25], @controller.api_offset_and_limit({:offset => -10}) - end + def test_api_offset_and_limit_with_page_and_limit + assert_equal [0, 100], @controller.api_offset_and_limit({:page => 1, :limit => 100}) + assert_equal [200, 100], @controller.api_offset_and_limit({:page => 3, :limit => 100}) + end - context "and limit" do - should "return offset, limit" do - assert_equal [10, 50], @controller.api_offset_and_limit({:offset => 10, :limit => 50}) - end - end - end + def test_unhautorized_exception_with_anonymous_should_redirect_to_login + WelcomeController.any_instance.stubs(:index).raises(::Unauthorized) - context "with page" do - should "return offset, 25" do - assert_equal [0, 25], @controller.api_offset_and_limit({:page => 1}) - assert_equal [50, 25], @controller.api_offset_and_limit({:page => 3}) - end + get :index + assert_response 302 + assert_redirected_to('/login?back_url='+CGI.escape('http://test.host/')) + end - should "not be negative" do - assert_equal [0, 25], @controller.api_offset_and_limit({:page => 0}) - assert_equal [0, 25], @controller.api_offset_and_limit({:page => -2}) - end + def test_unhautorized_exception_with_anonymous_and_xmlhttprequest_should_respond_with_401_to_anonymous + WelcomeController.any_instance.stubs(:index).raises(::Unauthorized) - context "and limit" do - should "return offset, limit" do - assert_equal [0, 100], @controller.api_offset_and_limit({:page => 1, :limit => 100}) - assert_equal [200, 100], @controller.api_offset_and_limit({:page => 3, :limit => 100}) - end - end - end + @request.env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" + get :index + assert_response 401 end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/wiki_controller_test.rb --- a/test/functional/wiki_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/wiki_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,10 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'wiki_controller' - -# Re-raise errors caught by the controller. -class WikiController; def rescue_action(e) raise e end; end class WikiControllerTest < ActionController::TestCase fixtures :projects, :users, :roles, :members, :member_roles, @@ -27,9 +23,6 @@ :wiki_content_versions, :attachments def setup - @controller = WikiController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end @@ -60,7 +53,7 @@ assert_tag :tag => 'h1', :content => /Another page/ # Included page with an inline image assert_tag :tag => 'p', :content => /This is an inline image/ - assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3', + assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3/logo.gif', :alt => 'This is a logo' } end @@ -184,6 +177,16 @@ assert_response 302 end + def test_show_page_without_content_should_display_the_edit_form + @request.session[:user_id] = 2 + WikiPage.create!(:title => 'NoContent', :wiki => Project.find(1).wiki) + + get :show, :project_id => 1, :id => 'NoContent' + assert_response :success + assert_template 'edit' + assert_select 'textarea[name=?]', 'content[text]' + end + def test_create_page @request.session[:user_id] = 2 assert_difference 'WikiPage.count' do @@ -419,6 +422,19 @@ assert_equal 2, c.version end + def test_update_page_without_content_should_create_content + @request.session[:user_id] = 2 + page = WikiPage.create!(:title => 'NoContent', :wiki => Project.find(1).wiki) + + assert_no_difference 'WikiPage.count' do + assert_difference 'WikiContent.count' do + put :update, :project_id => 1, :id => 'NoContent', :content => {:text => 'Some content'} + assert_response 302 + end + end + assert_equal 'Some content', page.reload.content.text + end + def test_update_section @request.session[:user_id] = 2 page = WikiPage.find_by_title('Page_with_sections') @@ -438,7 +454,7 @@ end end end - assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections' + assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections#section-2' assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text end @@ -461,7 +477,7 @@ end end end - assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections' + assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections#section-2' page.reload assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text assert_equal 4, page.content.version @@ -592,7 +608,7 @@ # Line 5 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => { - :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => { + :tag => 'td', :attributes => {:class => 'author'}, :content => /Redmine Admin/, :sibling => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ } } diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/wikis_controller_test.rb --- a/test/functional/wikis_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/wikis_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../test_helper', __FILE__) -require 'wikis_controller' - -# Re-raise errors caught by the controller. -class WikisController; def rescue_action(e) raise e end; end class WikisControllerTest < ActionController::TestCase fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis def setup - @controller = WikisController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new User.current = nil end diff -r 038ba2d95de8 -r 261b3d9a4903 test/functional/workflows_controller_test.rb --- a/test/functional/workflows_controller_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/functional/workflows_controller_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,18 +16,11 @@ # 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 @@ -37,7 +30,7 @@ assert_response :success assert_template 'index' - count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2') + count = WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count assert_tag :tag => 'a', :content => count.to_s, :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' } end @@ -102,9 +95,9 @@ } 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}) + assert_equal 3, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count + assert_not_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first + assert_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4).first end def test_post_edit_with_additional_transitions @@ -115,27 +108,27 @@ } assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' - assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + assert_equal 4, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count - w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5}) + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5).first 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}) + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1).first 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}) + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first 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}) + w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4).first assert w.author assert w.assignee end def test_clear_workflow - assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0 + assert WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count > 0 - post :edit, :role_id => 2, :tracker_id => 1 - assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + post :edit, :role_id => 1, :tracker_id => 2 + assert_equal 0, WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count end def test_get_permissions @@ -207,6 +200,23 @@ end end + def test_get_permissions_should_disable_hidden_custom_fields + cf1 = IssueCustomField.generate!(:tracker_ids => [1], :visible => true) + cf2 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1]) + cf3 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1, 2]) + + get :permissions, :role_id => 2, :tracker_id => 1 + assert_response :success + assert_template 'permissions' + + assert_select 'select[name=?]:not(.disabled)', "permissions[#{cf1.id}][1]" + assert_select 'select[name=?]:not(.disabled)', "permissions[#{cf3.id}][1]" + + assert_select 'select[name=?][disabled=disabled]', "permissions[#{cf2.id}][1]" do + assert_select 'option[value=][selected=selected]', :text => 'Hidden' + end + end + def test_get_permissions_with_role_and_tracker_and_all_statuses WorkflowTransition.delete_all @@ -304,9 +314,32 @@ assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3) end + def test_post_copy_with_incomplete_source_specification_should_fail + assert_no_difference 'WorkflowRule.count' do + post :copy, + :source_tracker_id => '', :source_role_id => '2', + :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] + assert_response 200 + assert_select 'div.flash.error', :text => 'Please select a source tracker or role' + end + end + + def test_post_copy_with_incomplete_target_specification_should_fail + assert_no_difference 'WorkflowRule.count' do + post :copy, + :source_tracker_id => '1', :source_role_id => '2', + :target_tracker_ids => ['2', '3'] + assert_response 200 + assert_select 'div.flash.error', :text => 'Please select target tracker(s) and role(s)' + end + 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]} + WorkflowTransition. + where(conditions). + order('tracker_id, role_id, old_status_id, new_status_id'). + all. + collect {|w| [w.old_status, w.new_status_id]} end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/account_test.rb --- a/test/integration/account_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/account_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,7 +18,7 @@ require File.expand_path('../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' rescue # Won't run some tests end @@ -45,7 +45,7 @@ # User logs in with 'autologin' checked post '/login', :username => user.login, :password => 'admin', :autologin => 1 assert_redirected_to '/my/page' - token = Token.find :first + token = Token.first assert_not_nil token assert_equal user, token.user assert_equal 'autologin', token.action @@ -59,7 +59,7 @@ user.update_attribute :last_login_on, nil assert_nil user.reload.last_login_on - # User comes back with his autologin cookie + # User comes back with user's autologin cookie cookies[:autologin] = token.value get '/my/page' assert_response :success @@ -68,6 +68,33 @@ assert_not_nil user.reload.last_login_on end + def test_autologin_should_use_autologin_cookie_name + Token.delete_all + Redmine::Configuration.stubs(:[]).with('autologin_cookie_name').returns('custom_autologin') + Redmine::Configuration.stubs(:[]).with('autologin_cookie_path').returns('/') + Redmine::Configuration.stubs(:[]).with('autologin_cookie_secure').returns(false) + + with_settings :autologin => '7' do + assert_difference 'Token.count' do + post '/login', :username => 'admin', :password => 'admin', :autologin => 1 + end + assert_response 302 + assert cookies['custom_autologin'].present? + token = cookies['custom_autologin'] + + # Session is cleared + reset! + cookies['custom_autologin'] = token + get '/my/page' + assert_response :success + + assert_difference 'Token.count', -1 do + post '/logout' + end + assert cookies['custom_autologin'].blank? + end + end + def test_lost_password Token.delete_all @@ -79,7 +106,7 @@ post "account/lost_password", :mail => 'jSmith@somenet.foo' assert_redirected_to "/login" - token = Token.find(:first) + token = Token.first assert_equal 'recovery', token.action assert_equal 'jsmith@somenet.foo', token.user.mail assert !token.expired? @@ -91,7 +118,9 @@ assert_select 'input[name=new_password]' assert_select 'input[name=new_password_confirmation]' - post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + post "account/lost_password", + :token => token.value, :new_password => 'newpass123', + :new_password_confirmation => 'newpass123' assert_redirected_to "/login" assert_equal 'Password was successfully updated.', flash[:notice] @@ -99,6 +128,35 @@ assert_equal 0, Token.count end + def test_user_with_must_change_passwd_should_be_forced_to_change_its_password + User.find_by_login('jsmith').update_attribute :must_change_passwd, true + + post '/login', :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/my/page' + follow_redirect! + assert_redirected_to '/my/password' + + get '/issues' + assert_redirected_to '/my/password' + end + + def test_user_with_must_change_passwd_should_be_able_to_change_its_password + User.find_by_login('jsmith').update_attribute :must_change_passwd, true + + post '/login', :username => 'jsmith', :password => 'jsmith' + assert_redirected_to '/my/page' + follow_redirect! + assert_redirected_to '/my/password' + follow_redirect! + assert_response :success + post '/my/password', :password => 'jsmith', :new_password => 'newpassword', :new_password_confirmation => 'newpassword' + assert_redirected_to '/my/account' + follow_redirect! + assert_response :success + + assert_equal false, User.find_by_login('jsmith').must_change_passwd? + end + def test_register_with_automatic_activation Setting.self_registration = '3' @@ -106,8 +164,10 @@ assert_response :success assert_template 'account/register' - post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", - :password => "newpass123", :password_confirmation => "newpass123"} + post 'account/register', + :user => {:login => "newuser", :language => "en", + :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} assert_redirected_to '/my/account' follow_redirect! assert_response :success @@ -122,8 +182,10 @@ def test_register_with_manual_activation Setting.self_registration = '2' - post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", - :password => "newpass123", :password_confirmation => "newpass123"} + post 'account/register', + :user => {:login => "newuser", :language => "en", + :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} assert_redirected_to '/login' assert !User.find_by_login('newuser').active? end @@ -132,12 +194,14 @@ Setting.self_registration = '1' Token.delete_all - post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", - :password => "newpass123", :password_confirmation => "newpass123"} + post 'account/register', + :user => {:login => "newuser", :language => "en", + :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} assert_redirected_to '/login' assert !User.find_by_login('newuser').active? - token = Token.find(:first) + token = Token.first assert_equal 'register', token.action assert_equal 'newuser@foo.bar', token.user.mail assert !token.expired? @@ -150,7 +214,9 @@ def test_onthefly_registration # disable registration Setting.self_registration = '0' - AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66}) + AuthSource.expects(:authenticate).returns( + {:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', + :mail => 'foo@bar.com', :auth_source_id => 66}) post '/login', :username => 'foo', :password => 'bar' assert_redirected_to '/my/page' @@ -164,7 +230,8 @@ def test_onthefly_registration_with_invalid_attributes # disable registration Setting.self_registration = '0' - AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66}) + AuthSource.expects(:authenticate).returns( + {:login => 'foo', :lastname => 'Smith', :auth_source_id => 66}) post '/login', :username => 'foo', :password => 'bar' assert_response :success @@ -174,7 +241,8 @@ assert_no_tag :input, :attributes => { :name => 'user[login]' } assert_no_tag :input, :attributes => { :name => 'user[password]' } - post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'} + post 'account/register', + :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'} assert_redirected_to '/my/account' user = User.find_by_login('foo') @@ -182,4 +250,49 @@ assert_equal 66, user.auth_source_id assert user.hashed_password.blank? end + + def test_registered_user_should_be_able_to_get_a_new_activation_email + Token.delete_all + + with_settings :self_registration => '1', :default_language => 'en' do + # register a new account + assert_difference 'User.count' do + assert_difference 'Token.count' do + post 'account/register', + :user => {:login => "newuser", :language => "en", + :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + end + end + user = User.order('id desc').first + assert_equal User::STATUS_REGISTERED, user.status + reset! + + # try to use "lost password" + assert_no_difference 'ActionMailer::Base.deliveries.size' do + post '/account/lost_password', :mail => 'newuser@foo.bar' + end + assert_redirected_to '/account/lost_password' + follow_redirect! + assert_response :success + assert_select 'div.flash', :text => /new activation email/ + assert_select 'div.flash a[href=/account/activation_email]' + + # request a new action activation email + assert_difference 'ActionMailer::Base.deliveries.size' do + get '/account/activation_email' + end + assert_redirected_to '/login' + token = Token.order('id desc').first + activation_path = "/account/activate?token=#{token.value}" + assert_include activation_path, mail_body(ActionMailer::Base.deliveries.last) + + # activate the account + get activation_path + assert_redirected_to '/login' + + post '/login', :username => 'newuser', :password => 'newpass123' + assert_redirected_to '/my/page' + end + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/admin_test.rb --- a/test/integration/admin_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/admin_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,8 +24,7 @@ :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules def test_add_user log_user("admin", "admin") diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/api_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/integration/api_test/api_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,41 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::ApiTest::ApiTest < Redmine::ApiTest::Base + fixtures :users + + def setup + Setting.rest_api_enabled = '1' + end + + def test_api_should_work_with_protect_from_forgery + ActionController::Base.allow_forgery_protection = true + assert_difference('User.count') do + post '/users.xml', { + :user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123'} + }, + credentials('admin') + assert_response 201 + end + ensure + ActionController::Base.allow_forgery_protection = false + end +end \ No newline at end of file diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/attachments_test.rb --- a/test/integration/api_test/attachments_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/attachments_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::AttachmentsTest < ActionController::IntegrationTest +class Redmine::ApiTest::AttachmentsTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, @@ -25,7 +25,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :attachments def setup diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/authentication_test.rb --- a/test/integration/api_test/authentication_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/authentication_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::AuthenticationTest < ActionController::IntegrationTest +class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base fixtures :users def setup diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/custom_fields_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/integration/api_test/custom_fields_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,43 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class Redmine::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 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/disabled_rest_api_test.rb --- a/test/integration/api_test/disabled_rest_api_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/disabled_rest_api_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,14 +1,30 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::DisabledRestApiTest < ActionController::IntegrationTest +class Redmine::ApiTest::DisabledRestApiTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules def setup Setting.rest_api_enabled = '0' diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/enumerations_test.rb --- a/test/integration/api_test/enumerations_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/enumerations_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,27 +17,21 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::EnumerationsTest < ActionController::IntegrationTest +class Redmine::ApiTest::EnumerationsTest < Redmine::ApiTest::Base 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 + test "GET /enumerations/issue_priorities.xml 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/groups_test.rb --- a/test/integration/api_test/groups_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/groups_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,196 +17,154 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::GroupsTest < ActionController::IntegrationTest +class Redmine::ApiTest::GroupsTest < Redmine::ApiTest::Base 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 + test "GET /groups.xml 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 + test "GET /groups.xml 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) + assert_select 'groups' do + assert_select 'group' do + assert_select 'name', :text => 'A Team' + assert_select 'id', :text => '10' 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 + test "GET /groups.json should require authentication" do + get '/groups.json' + assert_response 401 + end - assert_select 'group' do - assert_select 'name', :text => 'A Team' - assert_select 'id', :text => '10' - end - end + test "GET /groups.json should return groups" do + get '/groups.json', {}, credentials('admin') + assert_response :success + assert_equal 'application/json', response.content_type - should "include users if requested" do - get '/groups/10.xml?include=users', {}, credentials('admin') - assert_response :success - assert_equal 'application/xml', 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 - assert_select 'group' do - assert_select 'users' do - assert_select 'user', Group.find(10).users.count - assert_select 'user[id=8]' - end - end - end + test "GET /groups/:id.xml should return the group with its users" do + get '/groups/10.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type - 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 'name', :text => 'A Team' + assert_select 'id', :text => '10' + end + end - assert_select 'group' do - assert_select 'memberships' - end + test "GET /groups/:id.xml 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 - 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 + test "GET /groups/:id.xml 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 '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 + assert_select 'group' do + assert_select 'memberships' 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 + test "POST /groups.xml with valid parameters should create the group" 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 - 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 + group = Group.order('id DESC').first + assert_equal 'Test', group.name + assert_equal [2, 3], group.users.map(&:id).sort - assert_select 'errors' do - assert_select 'error', :text => /Name can't be blank/ - end - end - end + assert_select 'group' do + assert_select 'name', :text => 'Test' 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 + test "POST /groups.xml with invalid parameters 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 - 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 + test "PUT /groups/:id.xml with valid parameters 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 + + test "PUT /groups/:id.xml with invalid parameters 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 - 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 + test "DELETE /groups/:id.xml 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 + + test "POST /groups/:id/users.xml 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 + + test "DELETE /groups/:id/users/:user_id.xml 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/http_basic_login_test.rb --- a/test/integration/api_test/http_basic_login_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/http_basic_login_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,14 +1,30 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::HttpBasicLoginTest < ActionController::IntegrationTest +class Redmine::ApiTest::HttpBasicLoginTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules def setup Setting.rest_api_enabled = '1' diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/http_basic_login_with_api_token_test.rb --- a/test/integration/api_test/http_basic_login_with_api_token_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/http_basic_login_with_api_token_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,14 +1,30 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::HttpBasicLoginWithApiTokenTest < ActionController::IntegrationTest +class Redmine::ApiTest::HttpBasicLoginWithApiTokenTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules def setup Setting.rest_api_enabled = '1' diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/issue_categories_test.rb --- a/test/integration/api_test/issue_categories_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/issue_categories_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::IssueCategoriesTest < ActionController::IntegrationTest +class Redmine::ApiTest::IssueCategoriesTest < Redmine::ApiTest::Base fixtures :projects, :users, :issue_categories, :issues, :roles, :member_roles, @@ -28,99 +28,83 @@ 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 + 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 - 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 + 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 - 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 + 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 + 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 - 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 - assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + 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 - 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 + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/issue_relations_test.rb --- a/test/integration/api_test/issue_relations_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/issue_relations_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::IssueRelationsTest < ActionController::IntegrationTest +class Redmine::ApiTest::IssueRelationsTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, @@ -25,83 +25,68 @@ :member_roles, :members, :enabled_modules, - :workflows, :issue_relations def setup Setting.rest_api_enabled = '1' end - context "/issues/:issue_id/relations" do - context "GET" do - should "return issue relations" do - get '/issues/9/relations.xml', {}, credentials('jsmith') + test "GET /issues/:issue_id/relations.xml should return issue relations" do + get '/issues/9/relations.xml', {}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type + assert_response :success + assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'relations', - :attributes => { :type => 'array' }, - :child => { - :tag => 'relation', - :child => { - :tag => 'id', - :content => '1' - } - } - end + assert_tag :tag => 'relations', + :attributes => { :type => 'array' }, + :child => { + :tag => 'relation', + :child => { + :tag => 'id', + :content => '1' + } + } + end + + test "POST /issues/:issue_id/relations.xml should create the relation" do + assert_difference('IssueRelation.count') do + post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith') end - context "POST" do - should "create a relation" do - assert_difference('IssueRelation.count') do - post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith') - end + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_from_id + assert_equal 7, relation.issue_to_id + assert_equal 'relates', relation.relation_type - relation = IssueRelation.first(:order => 'id DESC') - assert_equal 2, relation.issue_from_id - assert_equal 7, relation.issue_to_id - assert_equal 'relates', relation.relation_type - - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s} - end - - context "with failure" do - should "return the errors" do - assert_no_difference('IssueRelation.count') do - post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith') - end - - assert_response :unprocessable_entity - assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/} - end - end - end + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s} end - context "/relations/:id" do - context "GET" do - should "return the relation" do - get '/relations/2.xml', {}, credentials('jsmith') - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag 'relation', :child => {:tag => 'id', :content => '2'} - end + test "POST /issues/:issue_id/relations.xml with failure should return errors" do + assert_no_difference('IssueRelation.count') do + post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith') end - context "DELETE" do - should "delete the relation" do - assert_difference('IssueRelation.count', -1) do - delete '/relations/2.xml', {}, credentials('jsmith') - end + assert_response :unprocessable_entity + assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/} + end - assert_response :ok - assert_equal '', @response.body - assert_nil IssueRelation.find_by_id(2) - end + test "GET /relations/:id.xml should return the relation" do + get '/relations/2.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag 'relation', :child => {:tag => 'id', :content => '2'} + end + + test "DELETE /relations/:id.xml should delete the relation" do + assert_difference('IssueRelation.count', -1) do + delete '/relations/2.xml', {}, credentials('jsmith') end + + assert_response :ok + assert_equal '', @response.body + assert_nil IssueRelation.find_by_id(2) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/issue_statuses_test.rb --- a/test/integration/api_test/issue_statuses_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/issue_statuses_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,35 +17,30 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::IssueStatusesTest < ActionController::IntegrationTest +class Redmine::ApiTest::IssueStatusesTest < Redmine::ApiTest::Base fixtures :issue_statuses def setup Setting.rest_api_enabled = '1' end - context "/issue_statuses" do - context "GET" do + test "GET /issue_statuses.xml should return issue statuses" do + get '/issue_statuses.xml' - should "return issue statuses" do - get '/issue_statuses.xml' - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'issue_statuses', - :attributes => {:type => 'array'}, - :child => { - :tag => 'issue_status', - :child => { - :tag => 'id', - :content => '2', - :sibling => { - :tag => 'name', - :content => 'Assigned' - } - } + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_statuses', + :attributes => {:type => 'array'}, + :child => { + :tag => 'issue_status', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => 'Assigned' } - end - end + } + } end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/issues_test.rb --- a/test/integration/api_test/issues_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/issues_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::IssuesTest < ActionController::IntegrationTest +class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base fixtures :projects, :users, :roles, @@ -138,9 +138,9 @@ 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) + expected_ids = Issue.visible. + joins(:custom_values). + where(: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 @@ -151,9 +151,9 @@ 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) + expected_ids = Issue.visible. + joins(:custom_values). + where(: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) } @@ -170,7 +170,7 @@ 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) + expected_ids = Issue.visible.where(: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) } @@ -453,6 +453,21 @@ end end + test "GET /issues/:id.xml?include=watchers should include watchers" do + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + + get '/issues/1.xml?include=watchers', {}, credentials('jsmith') + + assert_response :ok + assert_equal 'application/xml', response.content_type + assert_select 'issue' do + assert_select 'watchers', Issue.find(1).watchers.count + assert_select 'watchers' do + assert_select 'user[id=3]' + end + end + end + context "POST /issues.xml" do should_allow_api_authentication( :post, @@ -478,6 +493,18 @@ end end + test "POST /issues.xml with watcher_user_ids should create issue with watchers" do + assert_difference('Issue.count') do + post '/issues.xml', + {:issue => {:project_id => 1, :subject => 'Watchers', + :tracker_id => 2, :status_id => 3, :watcher_user_ids => [3, 1]}}, credentials('jsmith') + assert_response :created + end + issue = Issue.order('id desc').first + assert_equal 2, issue.watchers.size + assert_equal [1, 3], issue.watcher_user_ids.sort + end + context "POST /issues.xml with failure" do should "have an errors tag" do assert_no_difference('Issue.count') do @@ -720,6 +747,30 @@ end end + test "POST /issues/:id/watchers.xml should add watcher" do + assert_difference 'Watcher.count' do + post '/issues/1/watchers.xml', {:user_id => 3}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + watcher = Watcher.order('id desc').first + assert_equal Issue.find(1), watcher.watchable + assert_equal User.find(3), watcher.user + end + + test "DELETE /issues/:id/watchers/:user_id.xml should remove watcher" do + Watcher.create!(:user_id => 3, :watchable => Issue.find(1)) + + assert_difference 'Watcher.count', -1 do + delete '/issues/1/watchers/3.xml', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + assert_equal false, Issue.find(1).watched_by?(User.find(3)) + end + def test_create_issue_with_uploaded_file set_tmp_attachments_directory # upload the file diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/jsonp_test.rb --- a/test/integration/api_test/jsonp_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/jsonp_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,11 +17,23 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::JsonpTest < ActionController::IntegrationTest +class Redmine::ApiTest::JsonpTest < Redmine::ApiTest::Base fixtures :trackers + def test_should_ignore_jsonp_callback_with_jsonp_disabled + with_settings :jsonp_enabled => '0' do + get '/trackers.json?jsonp=handler' + end + + assert_response :success + assert_match %r{^\{"trackers":.+\}$}, response.body + assert_equal 'application/json; charset=utf-8', response.headers['Content-Type'] + end + def test_jsonp_should_accept_callback_param - get '/trackers.json?callback=handler' + with_settings :jsonp_enabled => '1' do + get '/trackers.json?callback=handler' + end assert_response :success assert_match %r{^handler\(\{"trackers":.+\}\)$}, response.body @@ -29,7 +41,9 @@ end def test_jsonp_should_accept_jsonp_param - get '/trackers.json?jsonp=handler' + with_settings :jsonp_enabled => '1' do + get '/trackers.json?jsonp=handler' + end assert_response :success assert_match %r{^handler\(\{"trackers":.+\}\)$}, response.body @@ -37,7 +51,9 @@ end def test_jsonp_should_strip_invalid_characters_from_callback - get '/trackers.json?callback=+-aA$1_' + with_settings :jsonp_enabled => '1' do + get '/trackers.json?callback=+-aA$1_' + end assert_response :success assert_match %r{^aA1_\(\{"trackers":.+\}\)$}, response.body @@ -45,7 +61,9 @@ end def test_jsonp_without_callback_should_return_json - get '/trackers.json?callback=' + with_settings :jsonp_enabled => '1' do + get '/trackers.json?callback=' + end assert_response :success assert_match %r{^\{"trackers":.+\}$}, response.body diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/memberships_test.rb --- a/test/integration/api_test/memberships_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/memberships_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,184 +17,156 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::MembershipsTest < ActionController::IntegrationTest +class Redmine::ApiTest::MembershipsTest < Redmine::ApiTest::Base 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') + test "GET /projects/:project_id/memberships.xml 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', + 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 => 'id', - :content => '2', - :sibling => { - :tag => 'user', - :attributes => {:id => '3', :name => 'Dave Lopper'}, - :sibling => { - :tag => 'roles', - :child => { - :tag => 'role', - :attributes => {:id => '2', :name => 'Developer'} - } - } - } + :tag => 'role', + :attributes => {:id => '2', :name => 'Developer'} } } - end - end + } + } + } + end - context "json" do - should "return memberships" do - get '/projects/1/memberships.json', {}, credentials('jsmith') + test "GET /projects/:project_id/memberships.json 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 + 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 - 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') + test "POST /projects/:project_id/memberships.xml should create the 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 + assert_response :created end end - context "/memberships/:id" do - context "GET" do - context "xml" do - should "return the membership" do - get '/memberships/2.xml', {}, credentials('jsmith') + test "POST /projects/:project_id/memberships.xml with invalid parameters should return errors" do + assert_no_difference 'Member.count' do + post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'membership', + 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 + + test "GET /memberships/:id.xml 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 => 'id', - :content => '2', - :sibling => { - :tag => 'user', - :attributes => {:id => '3', :name => 'Dave Lopper'}, - :sibling => { - :tag => 'roles', - :child => { - :tag => 'role', - :attributes => {:id => '2', :name => 'Developer'} - } - } - } + :tag => 'role', + :attributes => {:id => '2', :name => 'Developer'} } - end - end + } + } + } + end - context "json" do - should "return the membership" do - get '/memberships/2.json', {}, credentials('jsmith') + test "GET /memberships/:id.json 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 + 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 + + test "PUT /memberships/:id.xml should update the 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 - 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') + test "PUT /memberships/:id.xml with invalid parameters should return errors" do + put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith') - assert_response :ok - assert_equal '', @response.body - end - member = Member.find(2) - assert_equal [1,2], member.role_ids.sort - end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/} + end - should "return errors on failure" do - put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith') + test "DELETE /memberships/:id.xml should destroy the membership" do + assert_difference 'Member.count', -1 do + delete '/memberships/2.xml', {}, 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 + assert_response :ok + assert_equal '', @response.body end + assert_nil Member.find_by_id(2) + end - context "DELETE" do - context "xml" do - should "destroy membership" do - assert_difference 'Member.count', -1 do - delete '/memberships/2.xml', {}, credentials('jsmith') + test "DELETE /memberships/:id.xml 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 :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 + assert_response :unprocessable_entity end end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/news_test.rb --- a/test/integration/api_test/news_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/news_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,8 +16,8 @@ # 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 + +class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, @@ -25,74 +25,60 @@ :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' + should_allow_api_authentication(:get, "/projects/onlinestore/news.xml") + should_allow_api_authentication(:get, "/projects/onlinestore/news.json") - assert_tag :tag => 'news', - :attributes => {:type => 'array'}, - :child => { - :tag => 'news', - :child => { - :tag => 'id', - :content => '2' - } - } - end - end + test "GET /news.xml should return news" do + get '/news.xml' - 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 + assert_tag :tag => 'news', + :attributes => {:type => 'array'}, + :child => { + :tag => 'news', + :child => { + :tag => 'id', + :content => '2' + } + } end - context "GET /projects/:project_id/news" do - context ".xml" do - should_allow_api_authentication(:get, "/projects/onlinestore/news.xml") + test "GET /news.json should return news" do + get '/news.json' - should "return news" do - get '/projects/ecookbook/news.xml' + 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 - assert_tag :tag => 'news', - :attributes => {:type => 'array'}, - :child => { - :tag => 'news', - :child => { - :tag => 'id', - :content => '2' - } - } - end - end + test "GET /projects/:project_id/news.xml should return news" do + get '/projects/ecookbook/news.xml' - context ".json" do - should_allow_api_authentication(:get, "/projects/onlinestore/news.json") + assert_tag :tag => 'news', + :attributes => {:type => 'array'}, + :child => { + :tag => 'news', + :child => { + :tag => 'id', + :content => '2' + } + } + end - should "return news" do - get '/projects/ecookbook/news.json' + test "GET /projects/:project_id/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 + 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/projects_test.rb --- a/test/integration/api_test/projects_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/projects_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::ProjectsTest < ActionController::IntegrationTest +class Redmine::ApiTest::ProjectsTest < Redmine::ApiTest::Base 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, :issue_categories @@ -27,271 +27,209 @@ set_tmp_attachments_directory end - context "GET /projects" do - context ".xml" do - should "return projects" do - get '/projects.xml' - assert_response :success - assert_equal 'application/xml', @response.content_type + # TODO: A private project is needed because should_allow_api_authentication + # actually tests that authentication is *required*, not just allowed + should_allow_api_authentication(:get, "/projects/2.xml") + should_allow_api_authentication(:get, "/projects/2.json") + should_allow_api_authentication(:post, + '/projects.xml', + {:project => {:name => 'API test', :identifier => 'api-test'}}, + {:success_code => :created}) + should_allow_api_authentication(:put, + '/projects/2.xml', + {:project => {:name => 'API update'}}, + {:success_code => :ok}) + should_allow_api_authentication(:delete, + '/projects/2.xml', + {}, + {:success_code => :ok}) - assert_tag :tag => 'projects', - :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}} - end + test "GET /projects.xml should return projects" do + get '/projects.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag :tag => 'projects', + :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}} + end + + test "GET /projects.json should return projects" do + get '/projects.json' + assert_response :success + assert_equal 'application/json', @response.content_type + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['projects'] + assert_kind_of Hash, json['projects'].first + assert json['projects'].first.has_key?('id') + end + + test "GET /projects/:id.xml should return the project" do + get '/projects/1.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag :tag => 'project', + :child => {:tag => 'id', :content => '1'} + assert_tag :tag => 'custom_field', + :attributes => {:name => 'Development status'}, :content => 'Stable' + + assert_no_tag 'trackers' + assert_no_tag 'issue_categories' + end + + test "GET /projects/:id.json should return the project" do + get '/projects/1.json' + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['project'] + assert_equal 1, json['project']['id'] + end + + test "GET /projects/:id.xml with hidden custom fields should not display hidden custom fields" do + ProjectCustomField.find_by_name('Development status').update_attribute :visible, false + + get '/projects/1.xml' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_no_tag 'custom_field', + :attributes => {:name => 'Development status'} + end + + test "GET /projects/:id.xml with include=issue_categories should return categories" do + get '/projects/1.xml?include=issue_categories' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag 'issue_categories', + :attributes => {:type => 'array'}, + :child => { + :tag => 'issue_category', + :attributes => { + :id => '2', + :name => 'Recipes' + } + } + end + + test "GET /projects/:id.xml with include=trackers should return trackers" do + get '/projects/1.xml?include=trackers' + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag 'trackers', + :attributes => {:type => 'array'}, + :child => { + :tag => 'tracker', + :attributes => { + :id => '2', + :name => 'Feature request' + } + } + end + + test "POST /projects.xml with valid parameters should create the project" do + Setting.default_projects_modules = ['issue_tracking', 'repository'] + + assert_difference('Project.count') do + post '/projects.xml', + {:project => {:name => 'API test', :identifier => 'api-test'}}, + credentials('admin') end - context ".json" do - should "return projects" do - get '/projects.json' - assert_response :success - assert_equal 'application/json', @response.content_type + project = Project.first(:order => 'id DESC') + assert_equal 'API test', project.name + assert_equal 'api-test', project.identifier + assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort + assert_equal Tracker.all.size, project.trackers.size - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Array, json['projects'] - assert_kind_of Hash, json['projects'].first - assert json['projects'].first.has_key?('id') - end - end + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s} end - context "GET /projects/:id" do - context ".xml" do - # TODO: A private project is needed because should_allow_api_authentication - # actually tests that authentication is *required*, not just allowed - should_allow_api_authentication(:get, "/projects/2.xml") - - should "return requested project" do - get '/projects/1.xml' - assert_response :success - assert_equal 'application/xml', @response.content_type - - assert_tag :tag => 'project', - :child => {:tag => 'id', :content => '1'} - assert_tag :tag => 'custom_field', - :attributes => {:name => 'Development status'}, :content => 'Stable' - - assert_no_tag 'trackers' - assert_no_tag 'issue_categories' - end - - context "with hidden custom fields" do - setup do - ProjectCustomField.find_by_name('Development status').update_attribute :visible, false - end - - should "not display hidden custom fields" do - get '/projects/1.xml' - assert_response :success - assert_equal 'application/xml', @response.content_type - - assert_no_tag 'custom_field', - :attributes => {:name => 'Development status'} - end - end - - should "return categories with include=issue_categories" do - get '/projects/1.xml?include=issue_categories' - assert_response :success - assert_equal 'application/xml', @response.content_type - - assert_tag 'issue_categories', - :attributes => {:type => 'array'}, - :child => { - :tag => 'issue_category', - :attributes => { - :id => '2', - :name => 'Recipes' - } - } - end - - should "return trackers with include=trackers" do - get '/projects/1.xml?include=trackers' - assert_response :success - assert_equal 'application/xml', @response.content_type - - assert_tag 'trackers', - :attributes => {:type => 'array'}, - :child => { - :tag => 'tracker', - :attributes => { - :id => '2', - :name => 'Feature request' - } - } - end + test "POST /projects.xml should accept enabled_module_names attribute" do + assert_difference('Project.count') do + post '/projects.xml', + {:project => {:name => 'API test', :identifier => 'api-test', :enabled_module_names => ['issue_tracking', 'news', 'time_tracking']}}, + credentials('admin') end - context ".json" do - should_allow_api_authentication(:get, "/projects/2.json") - - should "return requested project" do - get '/projects/1.json' - - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Hash, json['project'] - assert_equal 1, json['project']['id'] - end - end + project = Project.first(:order => 'id DESC') + assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort end - context "POST /projects" do - context "with valid parameters" do - setup do - Setting.default_projects_modules = ['issue_tracking', 'repository'] - @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}} - end - - context ".xml" do - should_allow_api_authentication(:post, - '/projects.xml', - {:project => {:name => 'API test', :identifier => 'api-test'}}, - {:success_code => :created}) - - - should "create a project with the attributes" do - assert_difference('Project.count') do - post '/projects.xml', @parameters, credentials('admin') - end - - project = Project.first(:order => 'id DESC') - assert_equal 'API test', project.name - assert_equal 'api-test', project.identifier - assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort - assert_equal Tracker.all.size, project.trackers.size - - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s} - end - - should "accept enabled_module_names attribute" do - @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']}) - - assert_difference('Project.count') do - post '/projects.xml', @parameters, credentials('admin') - end - - project = Project.first(:order => 'id DESC') - assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort - end - - should "accept tracker_ids attribute" do - @parameters[:project].merge!({:tracker_ids => [1, 3]}) - - assert_difference('Project.count') do - post '/projects.xml', @parameters, credentials('admin') - end - - project = Project.first(:order => 'id DESC') - assert_equal [1, 3], project.trackers.map(&:id).sort - end - end + test "POST /projects.xml should accept tracker_ids attribute" do + assert_difference('Project.count') do + post '/projects.xml', + {:project => {:name => 'API test', :identifier => 'api-test', :tracker_ids => [1, 3]}}, + credentials('admin') end - context "with invalid parameters" do - setup do - @parameters = {:project => {:name => 'API test'}} - end - - context ".xml" do - should "return errors" do - assert_no_difference('Project.count') do - post '/projects.xml', @parameters, credentials('admin') - end - - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"} - end - end - end + project = Project.first(:order => 'id DESC') + assert_equal [1, 3], project.trackers.map(&:id).sort end - context "PUT /projects/:id" do - context "with valid parameters" do - setup do - @parameters = {:project => {:name => 'API update'}} - end - - context ".xml" do - should_allow_api_authentication(:put, - '/projects/2.xml', - {:project => {:name => 'API update'}}, - {:success_code => :ok}) - - should "update the project" do - assert_no_difference 'Project.count' do - put '/projects/2.xml', @parameters, credentials('jsmith') - end - assert_response :ok - assert_equal '', @response.body - assert_equal 'application/xml', @response.content_type - project = Project.find(2) - assert_equal 'API update', project.name - end - - should "accept enabled_module_names attribute" do - @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']}) - - assert_no_difference 'Project.count' do - put '/projects/2.xml', @parameters, credentials('admin') - end - assert_response :ok - assert_equal '', @response.body - project = Project.find(2) - assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort - end - - should "accept tracker_ids attribute" do - @parameters[:project].merge!({:tracker_ids => [1, 3]}) - - assert_no_difference 'Project.count' do - put '/projects/2.xml', @parameters, credentials('admin') - end - assert_response :ok - assert_equal '', @response.body - project = Project.find(2) - assert_equal [1, 3], project.trackers.map(&:id).sort - end - end + test "POST /projects.xml with invalid parameters should return errors" do + assert_no_difference('Project.count') do + post '/projects.xml', {:project => {:name => 'API test'}}, credentials('admin') end - context "with invalid parameters" do - setup do - @parameters = {:project => {:name => ''}} - end - - context ".xml" do - should "return errors" do - assert_no_difference('Project.count') do - put '/projects/2.xml', @parameters, credentials('admin') - 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 + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"} end - context "DELETE /projects/:id" do - context ".xml" do - should_allow_api_authentication(:delete, - '/projects/2.xml', - {}, - {:success_code => :ok}) + test "PUT /projects/:id.xml with valid parameters should update the project" do + assert_no_difference 'Project.count' do + put '/projects/2.xml', {:project => {:name => 'API update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'application/xml', @response.content_type + project = Project.find(2) + assert_equal 'API update', project.name + end - should "delete the project" do - assert_difference('Project.count',-1) do - delete '/projects/2.xml', {}, credentials('admin') - end - assert_response :ok - assert_equal '', @response.body - assert_nil Project.find_by_id(2) - end + test "PUT /projects/:id.xml should accept enabled_module_names attribute" do + assert_no_difference 'Project.count' do + put '/projects/2.xml', {:project => {:name => 'API update', :enabled_module_names => ['issue_tracking', 'news', 'time_tracking']}}, credentials('admin') end + assert_response :ok + assert_equal '', @response.body + project = Project.find(2) + assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort + end + + test "PUT /projects/:id.xml should accept tracker_ids attribute" do + assert_no_difference 'Project.count' do + put '/projects/2.xml', {:project => {:name => 'API update', :tracker_ids => [1, 3]}}, credentials('admin') + end + assert_response :ok + assert_equal '', @response.body + project = Project.find(2) + assert_equal [1, 3], project.trackers.map(&:id).sort + end + + test "PUT /projects/:id.xml with invalid parameters should return errors" do + assert_no_difference('Project.count') do + put '/projects/2.xml', {:project => {:name => ''}}, credentials('admin') + 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 /projects/:id.xml should delete the project" do + assert_difference('Project.count',-1) do + delete '/projects/2.xml', {}, credentials('admin') + end + assert_response :ok + assert_equal '', @response.body + assert_nil Project.find_by_id(2) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/queries_test.rb --- a/test/integration/api_test/queries_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/queries_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::QueriesTest < ActionController::IntegrationTest +class Redmine::ApiTest::QueriesTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, @@ -25,35 +25,29 @@ :member_roles, :members, :enabled_modules, - :workflows, :queries def setup Setting.rest_api_enabled = '1' end - context "/queries" do - context "GET" do + test "GET /queries.xml should return queries" do + get '/queries.xml' - should "return queries" do - get '/queries.xml' - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'queries', - :attributes => {:type => 'array'}, - :child => { - :tag => 'query', - :child => { - :tag => 'id', - :content => '4', - :sibling => { - :tag => 'name', - :content => 'Public query for all projects' - } - } + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'queries', + :attributes => {:type => 'array'}, + :child => { + :tag => 'query', + :child => { + :tag => 'id', + :content => '4', + :sibling => { + :tag => 'name', + :content => 'Public query for all projects' } - end - end + } + } end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/roles_test.rb --- a/test/integration/api_test/roles_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/roles_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,73 +17,59 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::RolesTest < ActionController::IntegrationTest +class Redmine::ApiTest::RolesTest < Redmine::ApiTest::Base fixtures :roles def setup Setting.rest_api_enabled = '1' end - context "/roles" do - context "GET" do - context "xml" do - should "return the roles" do - get '/roles.xml' + test "GET /roles.xml should return the roles" do + get '/roles.xml' - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_equal 3, assigns(:roles).size + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_equal 3, assigns(:roles).size - assert_tag :tag => 'roles', - :attributes => {:type => 'array'}, - :child => { - :tag => 'role', - :child => { - :tag => 'id', - :content => '2', - :sibling => { - :tag => 'name', - :content => 'Developer' - } - } - } - end - end - - context "json" do - should "return the roles" do - get '/roles.json' - - assert_response :success - assert_equal 'application/json', @response.content_type - assert_equal 3, assigns(:roles).size - - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Array, json['roles'] - assert_include({'id' => 2, 'name' => 'Developer'}, json['roles']) - end - end - end + assert_tag :tag => 'roles', + :attributes => {:type => 'array'}, + :child => { + :tag => 'role', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => 'Developer' + } + } + } end - context "/roles/:id" do - context "GET" do - context "xml" do - should "return the role" do - get '/roles/1.xml' + test "GET /roles.json should return the roles" do + get '/roles.json' - assert_response :success - assert_equal 'application/xml', @response.content_type + assert_response :success + assert_equal 'application/json', @response.content_type + assert_equal 3, assigns(:roles).size - assert_select 'role' do - assert_select 'name', :text => 'Manager' - assert_select 'role permissions[type=array]' do - assert_select 'permission', Role.find(1).permissions.size - assert_select 'permission', :text => 'view_issues' - end - end - end + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['roles'] + assert_include({'id' => 2, 'name' => 'Developer'}, json['roles']) + end + + test "GET /roles/:id.xml should return the role" do + get '/roles/1.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_select 'role' do + assert_select 'name', :text => 'Manager' + assert_select 'role permissions[type=array]' do + assert_select 'permission', Role.find(1).permissions.size + assert_select 'permission', :text => 'view_issues' end end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/time_entries_test.rb --- a/test/integration/api_test/time_entries_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/time_entries_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::TimeEntriesTest < ActionController::IntegrationTest +class Redmine::ApiTest::TimeEntriesTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, @@ -25,141 +25,118 @@ :member_roles, :members, :enabled_modules, - :workflows, :time_entries def setup Setting.rest_api_enabled = '1' end - context "GET /time_entries.xml" do - should "return time entries" do - get '/time_entries.xml', {}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'time_entries', - :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}} - end - - context "with limit" do - should "return limited results" do - get '/time_entries.xml?limit=2', {}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'time_entries', - :children => {:count => 2} - end - end + test "GET /time_entries.xml should return time entries" do + get '/time_entries.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'time_entries', + :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}} end - context "GET /time_entries/2.xml" do - should "return requested time entry" do - get '/time_entries/2.xml', {}, credentials('jsmith') - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'time_entry', - :child => {:tag => 'id', :content => '2'} - end + test "GET /time_entries.xml with limit should return limited results" do + get '/time_entries.xml?limit=2', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'time_entries', + :children => {:count => 2} end - context "POST /time_entries.xml" do - context "with issue_id" do - should "return create time entry" do - assert_difference 'TimeEntry.count' do - post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith') - end - assert_response :created - assert_equal 'application/xml', @response.content_type - - entry = TimeEntry.first(:order => 'id DESC') - assert_equal 'jsmith', entry.user.login - assert_equal Issue.find(1), entry.issue - assert_equal Project.find(1), entry.project - assert_equal Date.parse('2010-12-02'), entry.spent_on - assert_equal 3.5, entry.hours - assert_equal TimeEntryActivity.find(11), entry.activity - end - - should "accept custom fields" do - field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string') - - assert_difference 'TimeEntry.count' do - post '/time_entries.xml', {:time_entry => { - :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}] - }}, credentials('jsmith') - end - assert_response :created - assert_equal 'application/xml', @response.content_type - - entry = TimeEntry.first(:order => 'id DESC') - assert_equal 'accepted', entry.custom_field_value(field) - end - end - - context "with project_id" do - should "return create time entry" do - assert_difference 'TimeEntry.count' do - post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith') - end - assert_response :created - assert_equal 'application/xml', @response.content_type - - entry = TimeEntry.first(:order => 'id DESC') - assert_equal 'jsmith', entry.user.login - assert_nil entry.issue - assert_equal Project.find(1), entry.project - assert_equal Date.parse('2010-12-02'), entry.spent_on - assert_equal 3.5, entry.hours - assert_equal TimeEntryActivity.find(11), entry.activity - end - end - - context "with invalid parameters" do - should "return errors" do - assert_no_difference 'TimeEntry.count' do - post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith') - end - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - - assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"} - end - end + test "GET /time_entries/:id.xml should return the time entry" do + get '/time_entries/2.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'time_entry', + :child => {:tag => 'id', :content => '2'} end - context "PUT /time_entries/2.xml" do - context "with valid parameters" do - should "update time entry" do - assert_no_difference 'TimeEntry.count' do - put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith') - end - assert_response :ok - assert_equal '', @response.body - assert_equal 'API Update', TimeEntry.find(2).comments - end + test "POST /time_entries.xml with issue_id should create time entry" do + assert_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith') end + assert_response :created + assert_equal 'application/xml', @response.content_type - context "with invalid parameters" do - should "return errors" do - assert_no_difference 'TimeEntry.count' do - put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith') - end - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - - assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"} - end - end + entry = TimeEntry.first(:order => 'id DESC') + assert_equal 'jsmith', entry.user.login + assert_equal Issue.find(1), entry.issue + assert_equal Project.find(1), entry.project + assert_equal Date.parse('2010-12-02'), entry.spent_on + assert_equal 3.5, entry.hours + assert_equal TimeEntryActivity.find(11), entry.activity end - context "DELETE /time_entries/2.xml" do - should "destroy time entry" do - assert_difference 'TimeEntry.count', -1 do - delete '/time_entries/2.xml', {}, credentials('jsmith') - end - assert_response :ok - assert_equal '', @response.body - assert_nil TimeEntry.find_by_id(2) + test "POST /time_entries.xml with issue_id should accept custom fields" do + field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string') + + assert_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => { + :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}] + }}, credentials('jsmith') end + assert_response :created + assert_equal 'application/xml', @response.content_type + + entry = TimeEntry.first(:order => 'id DESC') + assert_equal 'accepted', entry.custom_field_value(field) + end + + test "POST /time_entries.xml with project_id should create time entry" do + assert_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + entry = TimeEntry.first(:order => 'id DESC') + assert_equal 'jsmith', entry.user.login + assert_nil entry.issue + assert_equal Project.find(1), entry.project + assert_equal Date.parse('2010-12-02'), entry.spent_on + assert_equal 3.5, entry.hours + assert_equal TimeEntryActivity.find(11), entry.activity + end + + test "POST /time_entries.xml with invalid parameters should return errors" do + assert_no_difference 'TimeEntry.count' do + post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"} + end + + test "PUT /time_entries/:id.xml with valid parameters should update time entry" do + assert_no_difference 'TimeEntry.count' do + put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'API Update', TimeEntry.find(2).comments + end + + test "PUT /time_entries/:id.xml with invalid parameters should return errors" do + assert_no_difference 'TimeEntry.count' do + put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"} + end + + test "DELETE /time_entries/:id.xml should destroy time entry" do + assert_difference 'TimeEntry.count', -1 do + delete '/time_entries/2.xml', {}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_nil TimeEntry.find_by_id(2) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/token_authentication_test.rb --- a/test/integration/api_test/token_authentication_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/token_authentication_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,14 +1,30 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::TokenAuthenticationTest < ActionController::IntegrationTest +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, - :workflows + :enabled_modules def setup Setting.rest_api_enabled = '1' @@ -21,13 +37,6 @@ end # Using the NewsController because it's a simple API. - context "get /news" do - context "in :xml format" do - should_allow_key_based_auth(:get, "/news.xml") - end - - context "in :json format" do - should_allow_key_based_auth(:get, "/news.json") - end - end + should_allow_key_based_auth(:get, "/news.xml") + should_allow_key_based_auth(:get, "/news.json") end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/trackers_test.rb --- a/test/integration/api_test/trackers_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/trackers_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,35 +17,30 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::TrackersTest < ActionController::IntegrationTest +class Redmine::ApiTest::TrackersTest < Redmine::ApiTest::Base fixtures :trackers def setup Setting.rest_api_enabled = '1' end - context "/trackers" do - context "GET" do + test "GET /trackers.xml should return trackers" do + get '/trackers.xml' - should "return trackers" do - get '/trackers.xml' - - assert_response :success - assert_equal 'application/xml', @response.content_type - assert_tag :tag => 'trackers', - :attributes => {:type => 'array'}, - :child => { - :tag => 'tracker', - :child => { - :tag => 'id', - :content => '2', - :sibling => { - :tag => 'name', - :content => 'Feature request' - } - } + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'trackers', + :attributes => {:type => 'array'}, + :child => { + :tag => 'tracker', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => 'Feature request' } - end - end + } + } end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/users_test.rb --- a/test/integration/api_test/users_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/users_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,332 +16,312 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../../test_helper', __FILE__) -require 'pp' -class ApiTest::UsersTest < ActionController::IntegrationTest + +class Redmine::ApiTest::UsersTest < Redmine::ApiTest::Base fixtures :users, :members, :member_roles, :roles, :projects def setup Setting.rest_api_enabled = '1' end - context "GET /users" do - should_allow_api_authentication(:get, "/users.xml") - should_allow_api_authentication(:get, "/users.json") + should_allow_api_authentication(:get, "/users.xml") + should_allow_api_authentication(:get, "/users.json") + should_allow_api_authentication(:post, + '/users.xml', + {:user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123' + }}, + {:success_code => :created}) + should_allow_api_authentication(:post, + '/users.json', + {:user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net' + }}, + {:success_code => :created}) + should_allow_api_authentication(:put, + '/users/2.xml', + {:user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + }}, + {:success_code => :ok}) + should_allow_api_authentication(:put, + '/users/2.json', + {:user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + }}, + {:success_code => :ok}) + should_allow_api_authentication(:delete, + '/users/2.xml', + {}, + {:success_code => :ok}) + should_allow_api_authentication(:delete, + '/users/2.xml', + {}, + {:success_code => :ok}) + + test "GET /users/:id.xml should return the user" do + get '/users/2.xml' + + assert_response :success + assert_tag :tag => 'user', + :child => {:tag => 'id', :content => '2'} end - context "GET /users/2" do - context ".xml" do - should "return requested user" do - get '/users/2.xml' + test "GET /users/:id.json should return the user" do + get '/users/2.json' - assert_response :success - assert_tag :tag => 'user', - :child => {:tag => 'id', :content => '2'} - end + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['user'] + assert_equal 2, json['user']['id'] + end - context "with include=memberships" do - should "include memberships" do - get '/users/2.xml?include=memberships' - - assert_response :success - assert_tag :tag => 'memberships', - :parent => {:tag => 'user'}, - :children => {:count => 1} - end - end + test "GET /users/:id.xml with include=memberships should include memberships" do + get '/users/2.xml?include=memberships' + + assert_response :success + assert_tag :tag => 'memberships', + :parent => {:tag => 'user'}, + :children => {:count => 1} + end + + test "GET /users/:id.json with include=memberships should include memberships" do + get '/users/2.json?include=memberships' + + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json['user']['memberships'] + assert_equal [{ + "id"=>1, + "project"=>{"name"=>"eCookbook", "id"=>1}, + "roles"=>[{"name"=>"Manager", "id"=>1}] + }], json['user']['memberships'] + end + + test "GET /users/current.xml should require authentication" do + get '/users/current.xml' + + assert_response 401 + end + + test "GET /users/current.xml should return current user" do + get '/users/current.xml', {}, credentials('jsmith') + + assert_tag :tag => 'user', + :child => {:tag => 'id', :content => '2'} + end + + test "GET /users/:id should not return login for other user" do + get '/users/3.xml', {}, credentials('jsmith') + assert_response :success + assert_no_tag 'user', :child => {:tag => 'login'} + end + + test "GET /users/:id should return login for current user" do + get '/users/2.xml', {}, credentials('jsmith') + assert_response :success + assert_tag 'user', :child => {:tag => 'login', :content => 'jsmith'} + end + + test "GET /users/:id should not return api_key for other user" do + get '/users/3.xml', {}, credentials('jsmith') + assert_response :success + assert_no_tag 'user', :child => {:tag => 'api_key'} + end + + test "GET /users/:id should return api_key for current user" do + get '/users/2.xml', {}, credentials('jsmith') + assert_response :success + assert_tag 'user', :child => {:tag => 'api_key', :content => User.find(2).api_key} + end + + test "GET /users/:id should not return status for standard user" do + get '/users/3.xml', {}, credentials('jsmith') + assert_response :success + assert_no_tag 'user', :child => {:tag => 'status'} + end + + test "GET /users/:id should return status for administrators" do + get '/users/2.xml', {}, credentials('admin') + assert_response :success + assert_tag 'user', :child => {:tag => 'status', :content => User.find(1).status.to_s} + end + + test "POST /users.xml with valid parameters should create the user" do + assert_difference('User.count') do + post '/users.xml', { + :user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123', + :mail_notification => 'only_assigned'} + }, + credentials('admin') end - context ".json" do - should "return requested user" do - get '/users/2.json' + user = User.first(:order => 'id DESC') + assert_equal 'foo', user.login + assert_equal 'Firstname', user.firstname + assert_equal 'Lastname', user.lastname + assert_equal 'foo@example.net', user.mail + assert_equal 'only_assigned', user.mail_notification + assert !user.admin? + assert user.check_password?('secret123') - assert_response :success - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Hash, json['user'] - assert_equal 2, json['user']['id'] - end - - context "with include=memberships" do - should "include memberships" do - get '/users/2.json?include=memberships' - - assert_response :success - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Array, json['user']['memberships'] - assert_equal [{ - "id"=>1, - "project"=>{"name"=>"eCookbook", "id"=>1}, - "roles"=>[{"name"=>"Manager", "id"=>1}] - }], json['user']['memberships'] - end - end - end + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s} end - context "GET /users/current" do - context ".xml" do - should "require authentication" do - get '/users/current.xml' + test "POST /users.json with valid parameters should create the user" do + assert_difference('User.count') do + post '/users.json', { + :user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123', + :mail_notification => 'only_assigned'} + }, + credentials('admin') + end - assert_response 401 - end + user = User.first(:order => 'id DESC') + assert_equal 'foo', user.login + assert_equal 'Firstname', user.firstname + assert_equal 'Lastname', user.lastname + assert_equal 'foo@example.net', user.mail + assert !user.admin? - should "return current user" do - get '/users/current.xml', {}, credentials('jsmith') - - assert_tag :tag => 'user', - :child => {:tag => 'id', :content => '2'} - end - end + assert_response :created + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['user'] + assert_equal user.id, json['user']['id'] end - context "POST /users" do - context "with valid parameters" do - setup do - @parameters = { - :user => { - :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', - :mail => 'foo@example.net', :password => 'secret123', - :mail_notification => 'only_assigned' - } - } - end - - context ".xml" do - should_allow_api_authentication(:post, - '/users.xml', - {:user => { - :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', - :mail => 'foo@example.net', :password => 'secret123' - }}, - {:success_code => :created}) - - should "create a user with the attributes" do - assert_difference('User.count') do - post '/users.xml', @parameters, credentials('admin') - end - - user = User.first(:order => 'id DESC') - assert_equal 'foo', user.login - assert_equal 'Firstname', user.firstname - assert_equal 'Lastname', user.lastname - assert_equal 'foo@example.net', user.mail - assert_equal 'only_assigned', user.mail_notification - assert !user.admin? - assert user.check_password?('secret123') - - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s} - end - end - - context ".json" do - should_allow_api_authentication(:post, - '/users.json', - {:user => { - :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', - :mail => 'foo@example.net' - }}, - {:success_code => :created}) - - should "create a user with the attributes" do - assert_difference('User.count') do - post '/users.json', @parameters, credentials('admin') - end - - user = User.first(:order => 'id DESC') - assert_equal 'foo', user.login - assert_equal 'Firstname', user.firstname - assert_equal 'Lastname', user.lastname - assert_equal 'foo@example.net', user.mail - assert !user.admin? - - assert_response :created - assert_equal 'application/json', @response.content_type - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert_kind_of Hash, json['user'] - assert_equal user.id, json['user']['id'] - end - end + test "POST /users.xml with with invalid parameters should return errors" do + assert_no_difference('User.count') do + post '/users.xml', {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}}, credentials('admin') end - context "with invalid parameters" do - setup do - @parameters = {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}} - end - - context ".xml" do - should "return errors" do - assert_no_difference('User.count') do - post '/users.xml', @parameters, credentials('admin') - end - - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - assert_tag 'errors', :child => { - :tag => 'error', - :content => "First name can't be blank" - } - end - end - - context ".json" do - should "return errors" do - assert_no_difference('User.count') do - post '/users.json', @parameters, credentials('admin') - end - - assert_response :unprocessable_entity - assert_equal 'application/json', @response.content_type - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert json.has_key?('errors') - assert_kind_of Array, json['errors'] - end - end - end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => { + :tag => 'error', + :content => "First name can't be blank" + } end - context "PUT /users/2" do - context "with valid parameters" do - setup do - @parameters = { - :user => { - :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', - :mail => 'jsmith@somenet.foo' - } - } - end - - context ".xml" do - should_allow_api_authentication(:put, - '/users/2.xml', - {:user => { - :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', - :mail => 'jsmith@somenet.foo' - }}, - {:success_code => :ok}) - - should "update user with the attributes" do - assert_no_difference('User.count') do - put '/users/2.xml', @parameters, credentials('admin') - end - - user = User.find(2) - assert_equal 'jsmith', user.login - assert_equal 'John', user.firstname - assert_equal 'Renamed', user.lastname - assert_equal 'jsmith@somenet.foo', user.mail - assert !user.admin? - - assert_response :ok - assert_equal '', @response.body - end - end - - context ".json" do - should_allow_api_authentication(:put, - '/users/2.json', - {:user => { - :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', - :mail => 'jsmith@somenet.foo' - }}, - {:success_code => :ok}) - - should "update user with the attributes" do - assert_no_difference('User.count') do - put '/users/2.json', @parameters, credentials('admin') - end - - user = User.find(2) - assert_equal 'jsmith', user.login - assert_equal 'John', user.firstname - assert_equal 'Renamed', user.lastname - assert_equal 'jsmith@somenet.foo', user.mail - assert !user.admin? - - assert_response :ok - assert_equal '', @response.body - end - end + test "POST /users.json with with invalid parameters should return errors" do + assert_no_difference('User.count') do + post '/users.json', {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}}, credentials('admin') end - context "with invalid parameters" do - setup do - @parameters = { - :user => { - :login => 'jsmith', :firstname => '', :lastname => 'Lastname', - :mail => 'foo' - } - } - end - - context ".xml" do - should "return errors" do - assert_no_difference('User.count') do - put '/users/2.xml', @parameters, credentials('admin') - end - - assert_response :unprocessable_entity - assert_equal 'application/xml', @response.content_type - assert_tag 'errors', :child => { - :tag => 'error', - :content => "First name can't be blank" - } - end - end - - context ".json" do - should "return errors" do - assert_no_difference('User.count') do - put '/users/2.json', @parameters, credentials('admin') - end - - assert_response :unprocessable_entity - assert_equal 'application/json', @response.content_type - json = ActiveSupport::JSON.decode(response.body) - assert_kind_of Hash, json - assert json.has_key?('errors') - assert_kind_of Array, json['errors'] - end - end - end + assert_response :unprocessable_entity + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert json.has_key?('errors') + assert_kind_of Array, json['errors'] end - context "DELETE /users/2" do - context ".xml" do - should_allow_api_authentication(:delete, - '/users/2.xml', - {}, - {:success_code => :ok}) - - should "delete user" do - assert_difference('User.count', -1) do - delete '/users/2.xml', {}, credentials('admin') - end - - assert_response :ok - assert_equal '', @response.body - end + test "PUT /users/:id.xml with valid parameters should update the user" do + assert_no_difference('User.count') do + put '/users/2.xml', { + :user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo'} + }, + credentials('admin') end - context ".json" do - should_allow_api_authentication(:delete, - '/users/2.xml', - {}, - {:success_code => :ok}) + user = User.find(2) + assert_equal 'jsmith', user.login + assert_equal 'John', user.firstname + assert_equal 'Renamed', user.lastname + assert_equal 'jsmith@somenet.foo', user.mail + assert !user.admin? - should "delete user" do - assert_difference('User.count', -1) do - delete '/users/2.json', {}, credentials('admin') - end + assert_response :ok + assert_equal '', @response.body + end - assert_response :ok - assert_equal '', @response.body - end + test "PUT /users/:id.json with valid parameters should update the user" do + assert_no_difference('User.count') do + put '/users/2.json', { + :user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo'} + }, + credentials('admin') end + + user = User.find(2) + assert_equal 'jsmith', user.login + assert_equal 'John', user.firstname + assert_equal 'Renamed', user.lastname + assert_equal 'jsmith@somenet.foo', user.mail + assert !user.admin? + + assert_response :ok + assert_equal '', @response.body + end + + test "PUT /users/:id.xml with invalid parameters" do + assert_no_difference('User.count') do + put '/users/2.xml', { + :user => { + :login => 'jsmith', :firstname => '', :lastname => 'Lastname', + :mail => 'foo'} + }, + credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => { + :tag => 'error', + :content => "First name can't be blank" + } + end + + test "PUT /users/:id.json with invalid parameters" do + assert_no_difference('User.count') do + put '/users/2.json', { + :user => { + :login => 'jsmith', :firstname => '', :lastname => 'Lastname', + :mail => 'foo'} + }, + credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert json.has_key?('errors') + assert_kind_of Array, json['errors'] + end + + test "DELETE /users/:id.xml should delete the user" do + assert_difference('User.count', -1) do + delete '/users/2.xml', {}, credentials('admin') + end + + assert_response :ok + assert_equal '', @response.body + end + + test "DELETE /users/:id.json should delete the user" do + assert_difference('User.count', -1) do + delete '/users/2.json', {}, credentials('admin') + end + + assert_response :ok + assert_equal '', @response.body end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/versions_test.rb --- a/test/integration/api_test/versions_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/versions_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::VersionsTest < ActionController::IntegrationTest +class Redmine::ApiTest::VersionsTest < Redmine::ApiTest::Base fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories, :projects_trackers, @@ -25,112 +25,118 @@ :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' + test "GET /projects/:project_id/versions.xml 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' - } - } + 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 + + test "POST /projects/:project_id/versions.xml should create the version" do + assert_difference 'Version.count' do + post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith') 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 - 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 - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} - end + test "POST /projects/:project_id/versions.xml 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 - 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 - 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 - assert_response :created - assert_equal 'application/xml', @response.content_type - assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} - end + test "POST /projects/:project_id/versions.xml should create the version with custom fields" do + field = VersionCustomField.generate! - 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_difference 'Version.count' do + post '/projects/1/versions.xml', { + :version => { + :name => 'API test', + :custom_fields => [ + {'id' => field.id.to_s, 'value' => 'Some value'} + ] + } + }, credentials('jsmith') + end - assert_response :unprocessable_entity - assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"} - end - end + version = Version.first(:order => 'id DESC') + assert_equal 'API test', version.name + assert_equal 'Some value', version.custom_field_value(field) + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_select 'version>custom_fields>custom_field[id=?]>value', field.id.to_s, 'Some value' + end + + test "POST /projects/:project_id/versions.xml with failure 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 + + test "GET /versions/:id.xml 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 - context "/versions/:id" do - context "GET" do - should "return the version" do - get '/versions/2.xml' + test "PUT /versions/:id.xml should update the version" do + put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith') - 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 + assert_response :ok + assert_equal '', @response.body + assert_equal 'API update', Version.find(2).name + end + + test "DELETE /versions/:id.xml should destroy the version" do + assert_difference 'Version.count', -1 do + delete '/versions/3.xml', {}, credentials('jsmith') 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 + assert_response :ok + assert_equal '', @response.body + assert_nil Version.find_by_id(3) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/api_test/wiki_pages_test.rb --- a/test/integration/api_test/wiki_pages_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/api_test/wiki_pages_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,7 +17,7 @@ require File.expand_path('../../../test_helper', __FILE__) -class ApiTest::WikiPagesTest < ActionController::IntegrationTest +class Redmine::ApiTest::WikiPagesTest < Redmine::ApiTest::Base fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments @@ -90,6 +90,7 @@ assert_select 'version', :text => '2' assert_select 'text' assert_select 'author' + assert_select 'comments', :text => 'Small update' assert_select 'created_on' assert_select 'updated_on' end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/application_test.rb --- a/test/integration/application_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/application_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -26,8 +26,7 @@ :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules def test_set_localization Setting.default_language = 'en' @@ -37,17 +36,20 @@ assert_response :success assert_tag :tag => 'h2', :content => 'Projets' assert_equal :fr, current_language + assert_select "html[lang=?]", "fr" # 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 + assert_select "html[lang=?]", "it" # not a supported language: default language should be used get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'zz' assert_response :success assert_tag :tag => 'h2', :content => 'Projects' + assert_select "html[lang=?]", "en" end def test_token_based_access_should_not_start_session @@ -65,4 +67,13 @@ get '/login.png' assert_response 404 end + + def test_invalid_token_should_call_custom_handler + ActionController::Base.allow_forgery_protection = true + post '/issues' + assert_response 422 + assert_include "Invalid form authenticity token.", response.body + ensure + ActionController::Base.allow_forgery_protection = false + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/attachments_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/integration/attachments_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,132 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AttachmentsTest < ActionController::IntegrationTest + fixtures :projects, :enabled_modules, + :users, :roles, :members, :member_roles, + :trackers, :projects_trackers, + :issue_statuses, :enumerations + + def test_upload_as_js_and_attach_to_an_issue + log_user('jsmith', 'jsmith') + + token = ajax_upload('myupload.txt', 'File content') + + assert_difference 'Issue.count' do + post '/projects/ecookbook/issues', { + :issue => {:tracker_id => 1, :subject => 'Issue with upload'}, + :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} + } + assert_response 302 + end + + issue = Issue.order('id DESC').first + assert_equal 'Issue with upload', issue.subject + assert_equal 1, issue.attachments.count + + attachment = issue.attachments.first + assert_equal 'myupload.txt', attachment.filename + assert_equal 'My uploaded file', attachment.description + assert_equal 'File content'.length, attachment.filesize + end + + def test_upload_as_js_and_preview_as_inline_attachment + log_user('jsmith', 'jsmith') + + token = ajax_upload('myupload.jpg', 'JPEG content') + + post '/issues/preview/new/ecookbook', { + :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'}, + :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}} + } + assert_response :success + + attachment_path = response.body.match(%r{ {:tracker_id => 1, :subject => ''}, + :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} + } + assert_response :success + end + assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token + assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt' + assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file' + + assert_difference 'Issue.count' do + post '/projects/ecookbook/issues', { + :issue => {:tracker_id => 1, :subject => 'Issue with upload'}, + :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} + } + assert_response 302 + end + + issue = Issue.order('id DESC').first + assert_equal 'Issue with upload', issue.subject + assert_equal 1, issue.attachments.count + + attachment = issue.attachments.first + assert_equal 'myupload.txt', attachment.filename + assert_equal 'My uploaded file', attachment.description + assert_equal 'File content'.length, attachment.filesize + end + + def test_upload_as_js_and_destroy + log_user('jsmith', 'jsmith') + + token = ajax_upload('myupload.txt', 'File content') + + attachment = Attachment.order('id DESC').first + attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1" + assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}" + + assert_difference 'Attachment.count', -1 do + delete attachment_path + assert_response :success + end + + assert_include "$('#attachments_1').remove();", response.body + end + + private + + def ajax_upload(filename, content, attachment_id=1) + assert_difference 'Attachment.count' do + post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'} + assert_response :success + assert_equal 'text/javascript', response.content_type + end + + token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1] + assert_not_nil token, "No upload token found in response:\n#{response.body}" + token + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/issues_test.rb --- a/test/integration/issues_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/issues_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -65,15 +65,6 @@ 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') diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/layout_test.rb --- a/test/integration/layout_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/layout_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,8 +24,7 @@ :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules test "browsing to a missing page should render the base layout" do get "/users/100000000" @@ -86,6 +85,26 @@ 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/lib/redmine/hook_test.rb --- a/test/integration/lib/redmine/hook_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/lib/redmine/hook_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/lib/redmine/menu_manager_test.rb --- a/test/integration/lib/redmine/menu_manager_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/lib/redmine/menu_manager_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -26,8 +26,7 @@ :roles, :member_roles, :members, - :enabled_modules, - :workflows + :enabled_modules def test_project_menu_with_specific_locale get 'projects/ecookbook/issues', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/lib/redmine/themes_test.rb --- a/test/integration/lib/redmine/themes_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/lib/redmine/themes_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/projects_test.rb --- a/test/integration/projects_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/projects_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/repositories_git_test.rb --- a/test/integration/repositories_git_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/repositories_git_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/account_test.rb --- a/test/integration/routing/account_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/account_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,10 +25,12 @@ { :controller => 'account', :action => 'login' } ) end - assert_routing( - { :method => 'get', :path => "/logout" }, - { :controller => 'account', :action => 'logout' } - ) + ["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" }, diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/activities_test.rb --- a/test/integration/routing/activities_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/activities_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/admin_test.rb --- a/test/integration/routing/admin_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/admin_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/attachments_test.rb --- a/test/integration/routing/attachments_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/attachments_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/auth_sources_test.rb --- a/test/integration/routing/auth_sources_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/auth_sources_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -51,5 +51,9 @@ { :controller => 'auth_sources', :action => 'test_connection', :id => '1234' } ) + assert_routing( + { :method => 'get', :path => "/auth_sources/autocomplete_for_new_user" }, + { :controller => 'auth_sources', :action => 'autocomplete_for_new_user' } + ) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/auto_completes_test.rb --- a/test/integration/routing/auto_completes_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/auto_completes_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/boards_test.rb --- a/test/integration/routing/boards_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/boards_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/calendars_test.rb --- a/test/integration/routing/calendars_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/calendars_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/comments_test.rb --- a/test/integration/routing/comments_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/comments_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/context_menus_test.rb --- a/test/integration/routing/context_menus_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/context_menus_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/custom_fields_test.rb --- a/test/integration/routing/custom_fields_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/custom_fields_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -44,4 +44,11 @@ { :controller => 'custom_fields', :action => 'destroy', :id => '2' } ) end + + def test_custom_fields_api + assert_routing( + { :method => 'get', :path => "/custom_fields.xml" }, + { :controller => 'custom_fields', :action => 'index', :format => 'xml' } + ) + end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/documents_test.rb --- a/test/integration/routing/documents_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/documents_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/enumerations_test.rb --- a/test/integration/routing/enumerations_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/enumerations_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/files_test.rb --- a/test/integration/routing/files_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/files_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/gantts_test.rb --- a/test/integration/routing/gantts_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/gantts_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/groups_test.rb --- a/test/integration/routing/groups_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/groups_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -48,6 +48,10 @@ { :controller => 'groups', :action => 'autocomplete_for_user', :id => '1' } ) assert_routing( + { :method => 'get', :path => "/groups/1/autocomplete_for_user.js" }, + { :controller => 'groups', :action => 'autocomplete_for_user', :id => '1', :format => 'js' } + ) + assert_routing( { :method => 'get', :path => "/groups/1" }, { :controller => 'groups', :action => 'show', :id => '1' } ) diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/issue_categories_test.rb --- a/test/integration/routing/issue_categories_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/issue_categories_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/issue_relations_test.rb --- a/test/integration/routing/issue_relations_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/issue_relations_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/issue_statuses_test.rb --- a/test/integration/routing/issue_statuses_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/issue_statuses_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/issues_test.rb --- a/test/integration/routing/issues_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/issues_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -107,8 +107,8 @@ def test_issues_form_update ["post", "put"].each do |method| assert_routing( - { :method => method, :path => "/projects/23/issues/new" }, - { :controller => 'issues', :action => 'new', :project_id => '23' } + { :method => method, :path => "/projects/23/issues/update_form" }, + { :controller => 'issues', :action => 'update_form', :project_id => '23' } ) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/journals_test.rb --- a/test/integration/routing/journals_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/journals_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/mail_handler_test.rb --- a/test/integration/routing/mail_handler_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/mail_handler_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/members_test.rb --- a/test/integration/routing/members_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/members_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -55,5 +55,9 @@ { :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 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/messages_test.rb --- a/test/integration/routing/messages_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/messages_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/my_test.rb --- a/test/integration/routing/my_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/my_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/news_test.rb --- a/test/integration/routing/news_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/news_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/previews_test.rb --- a/test/integration/routing/previews_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/previews_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,7 +19,7 @@ class RoutingPreviewsTest < ActionController::IntegrationTest def test_previews - ["get", "post"].each do |method| + ["get", "post", "put"].each do |method| assert_routing( { :method => method, :path => "/issues/preview/new/123" }, { :controller => 'previews', :action => 'issue', :project_id => '123' } diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/project_enumerations_test.rb --- a/test/integration/routing/project_enumerations_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/project_enumerations_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/projects_test.rb --- a/test/integration/routing/projects_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/projects_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/queries_test.rb --- a/test/integration/routing/queries_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/queries_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/reports_test.rb --- a/test/integration/routing/reports_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/reports_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/repositories_test.rb --- a/test/integration/routing/repositories_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/repositories_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/roles_test.rb --- a/test/integration/routing/roles_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/roles_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/search_test.rb --- a/test/integration/routing/search_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/search_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/settings_test.rb --- a/test/integration/routing/settings_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/settings_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/sys_test.rb --- a/test/integration/routing/sys_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/sys_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/timelog_test.rb --- a/test/integration/routing/timelog_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/timelog_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/trackers_test.rb --- a/test/integration/routing/trackers_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/trackers_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/users_test.rb --- a/test/integration/routing/users_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/users_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/versions_test.rb --- a/test/integration/routing/versions_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/versions_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/watchers_test.rb --- a/test/integration/routing/watchers_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/watchers_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -32,7 +32,7 @@ { :controller => 'watchers', :action => 'create' } ) assert_routing( - { :method => 'post', :path => "/watchers/destroy" }, + { :method => 'delete', :path => "/watchers" }, { :controller => 'watchers', :action => 'destroy' } ) assert_routing( @@ -44,8 +44,18 @@ { :controller => 'watchers', :action => 'watch' } ) assert_routing( - { :method => 'post', :path => "/watchers/unwatch" }, + { :method => 'delete', :path => "/watchers/watch" }, { :controller => 'watchers', :action => 'unwatch' } ) + assert_routing( + { :method => 'post', :path => "/issues/12/watchers.xml" }, + { :controller => 'watchers', :action => 'create', + :object_type => 'issue', :object_id => '12', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/12/watchers/3.xml" }, + { :controller => 'watchers', :action => 'destroy', + :object_type => 'issue', :object_id => '12', :user_id => '3', :format => 'xml'} + ) end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/welcome_test.rb --- a/test/integration/routing/welcome_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/welcome_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/wiki_test.rb --- a/test/integration/routing/wiki_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/wiki_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -53,6 +53,10 @@ { :controller => 'wiki', :action => 'annotate', :project_id => '1', :id => 'CookBook_documentation', :version => '2' } ) + # Make sure we don't route wiki page sub-uris to let plugins handle them + assert_raise(ActionController::RoutingError) do + assert_recognizes({}, {:method => 'get', :path => "/projects/1/wiki/CookBook_documentation/whatever"}) + end end def test_wiki_misc diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/wikis_test.rb --- a/test/integration/routing/wikis_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/wikis_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/routing/workflows_test.rb --- a/test/integration/routing/workflows_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/routing/workflows_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/integration/users_test.rb --- a/test/integration/users_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/integration/users_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/object_helpers.rb --- a/test/object_helpers.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/object_helpers.rb Tue Jan 14 14:37:42 2014 +0000 @@ -3,7 +3,7 @@ @generated_user_login ||= 'user0' @generated_user_login.succ! user = User.new(attributes) - user.login = @generated_user_login if user.login.blank? + 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? @@ -22,7 +22,7 @@ @generated_group_name ||= 'Group 0' @generated_group_name.succ! group = Group.new(attributes) - group.name = @generated_group_name if group.name.blank? + group.name = @generated_group_name.dup if group.name.blank? yield group if block_given? group.save! group @@ -32,18 +32,24 @@ @generated_project_identifier ||= 'project-0000' @generated_project_identifier.succ! project = Project.new(attributes) - project.name = @generated_project_identifier if project.name.blank? - project.identifier = @generated_project_identifier if project.identifier.blank? + 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 if tracker.name.blank? + tracker.name = @generated_tracker_name.dup if tracker.name.blank? yield tracker if block_given? tracker.save! tracker @@ -53,19 +59,26 @@ @generated_role_name ||= 'Role 0' @generated_role_name.succ! role = Role.new(attributes) - role.name = @generated_role_name if role.name.blank? + role.name = @generated_role_name.dup if role.name.blank? yield role if block_given? role.save! role end - def Issue.generate!(attributes={}) + # 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 @@ -92,17 +105,29 @@ @generated_version_name ||= 'Version 0' @generated_version_name.succ! version = Version.new(attributes) - version.name = @generated_version_name if version.name.blank? + 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 if source.name.blank? + source.name = @generated_auth_source_name.dup if source.name.blank? yield source if block_given? source.save! source @@ -112,8 +137,8 @@ @generated_board_name ||= 'Forum 0' @generated_board_name.succ! board = Board.new(attributes) - board.name = @generated_board_name if board.name.blank? - board.description = @generated_board_name if board.description.blank? + 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 @@ -126,8 +151,31 @@ attachment = Attachment.new(attributes) attachment.container ||= Issue.find(1) attachment.author ||= User.find(2) - attachment.filename = @generated_filename if attachment.filename.blank? + 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 038ba2d95de8 -r 261b3d9a4903 test/test_helper.rb --- a/test/test_helper.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/test_helper.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -26,30 +26,10 @@ class ActiveSupport::TestCase include ActionDispatch::TestProcess - - # Transactional fixtures accelerate your tests by wrapping each test method - # in a transaction that's rolled back on completion. This ensures that the - # test database remains unchanged so your fixtures don't have to be reloaded - # between every test method. Fewer database queries means faster tests. - # - # Read Mike Clark's excellent walkthrough at - # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting - # - # Every Active Record database supports transactions except MyISAM tables - # in MySQL. Turn off transactional fixtures in this case; however, if you - # don't care one way or the other, switching from MyISAM to InnoDB tables - # is recommended. + self.use_transactional_fixtures = true - - # Instantiated fixtures are slow, but give you @david where otherwise you - # would need people(:david). If you don't want to migrate your existing - # test cases which use the @david style and don't mind the speed hit (each - # instantiated fixtures translates to a database query per test method), - # then set this back to true. self.use_instantiated_fixtures = false - # Add more helper methods to be used by all tests here... - def log_user(login, password) User.anonymous get "/login" @@ -107,7 +87,15 @@ end def with_settings(options, &block) - saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].is_a?(Symbol) ? Setting[k] : Setting[k].dup; h} + saved_settings = options.keys.inject({}) do |h, k| + h[k] = case Setting[k] + when Symbol, false, true, nil + Setting[k] + else + Setting[k].dup + end + h + end options.each {|k, v| Setting[k] = v} yield ensure @@ -124,7 +112,7 @@ end def change_user_password(login, new_password) - user = User.first(:conditions => {:login => login}) + user = User.where(:login => login).first user.password, user.password_confirmation = new_password, new_password user.save! end @@ -181,8 +169,8 @@ assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"") end - def assert_not_include(expected, s) - assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\"" + def assert_not_include(expected, s, message=nil) + assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"") end def assert_select_in(text, *args, &block) @@ -190,305 +178,291 @@ assert_select(d, *args, &block) end - def assert_mail_body_match(expected, mail) + def assert_mail_body_match(expected, mail, message=nil) if expected.is_a?(String) - assert_include expected, mail_body(mail) + assert_include expected, mail_body(mail), message else - assert_match expected, mail_body(mail) + assert_match expected, mail_body(mail), message end end - def assert_mail_body_no_match(expected, mail) + def assert_mail_body_no_match(expected, mail, message=nil) if expected.is_a?(String) - assert_not_include expected, mail_body(mail) + assert_not_include expected, mail_body(mail), message else - assert_no_match expected, mail_body(mail) + assert_no_match expected, mail_body(mail), message end end def mail_body(mail) mail.parts.first.body.encoded end +end - # Shoulda macros - def self.should_render_404 - should_respond_with :not_found - should_render_template 'common/error' - end - - def self.should_have_before_filter(expected_method, options = {}) - should_have_filter('before', expected_method, options) - end - - def self.should_have_after_filter(expected_method, options = {}) - should_have_filter('after', expected_method, options) - end - - def self.should_have_filter(filter_type, expected_method, options) - description = "have #{filter_type}_filter :#{expected_method}" - description << " with #{options.inspect}" unless options.empty? - - should description do - klass = "action_controller/filters/#{filter_type}_filter".classify.constantize - expected = klass.new(:filter, expected_method.to_sym, options) - assert_equal 1, @controller.class.filter_chain.select { |filter| - filter.method == expected.method && filter.kind == expected.kind && - filter.options == expected.options && filter.class == expected.class - }.size - end - end - - # Test that a request allows the three types of API authentication - # - # * HTTP Basic with username and password - # * HTTP Basic with an api key for the username - # * Key based with the key=X parameter - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_api_authentication(http_method, url, parameters={}, options={}) - should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options) - should_allow_http_basic_auth_with_key(http_method, url, parameters, options) - should_allow_key_based_auth(http_method, url, parameters, options) - end - - # Test that a request allows the username and password for HTTP BASIC - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={}) - success_code = options[:success_code] || :success - failure_code = options[:failure_code] || :unauthorized - - context "should allow http basic auth using a username and password for #{http_method} #{url}" do - context "with a valid HTTP authentication" do - setup do - @user = User.generate! do |user| - user.admin = true - user.password = 'my_password' +module Redmine + module ApiTest + # Base class for API tests + class Base < ActionDispatch::IntegrationTest + # Test that a request allows the three types of API authentication + # + # * HTTP Basic with username and password + # * HTTP Basic with an api key for the username + # * Key based with the key=X parameter + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_api_authentication(http_method, url, parameters={}, options={}) + should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options) + should_allow_http_basic_auth_with_key(http_method, url, parameters, options) + should_allow_key_based_auth(http_method, url, parameters, options) + end + + # Test that a request allows the username and password for HTTP BASIC + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow http basic auth using a username and password for #{http_method} #{url}" do + context "with a valid HTTP authentication" do + setup do + @user = User.generate! do |user| + user.admin = true + user.password = 'my_password' + end + send(http_method, url, parameters, credentials(@user.login, 'my_password')) + end + + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end end - send(http_method, url, parameters, credentials(@user.login, 'my_password')) - end - - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current + + context "with an invalid HTTP authentication" do + setup do + @user = User.generate! + send(http_method, url, parameters, credentials(@user.login, 'wrong_password')) + end + + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end + + context "without credentials" do + setup do + send(http_method, url, parameters) + end + + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "include_www_authenticate_header" do + assert @controller.response.headers.has_key?('WWW-Authenticate') + end + end end end - - context "with an invalid HTTP authentication" do - setup do - @user = User.generate! - send(http_method, url, parameters, credentials(@user.login, 'wrong_password')) - end - - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "not login as the user" do - assert_equal User.anonymous, User.current + + # Test that a request allows the API key with HTTP BASIC + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow http basic auth with a key for #{http_method} #{url}" do + context "with a valid HTTP authentication using the API token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + send(http_method, url, parameters, credentials(@token.value, 'X')) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid HTTP authentication" do + setup do + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'feeds') + send(http_method, url, parameters, credentials(@token.value, 'X')) + end + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end end end - - context "without credentials" do - setup do - send(http_method, url, parameters) + + # Test that a request allows full key authentication + # + # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) + # @param [String] url the request url, without the key=ZXY parameter + # @param [optional, Hash] parameters additional request parameters + # @param [optional, Hash] options additional options + # @option options [Symbol] :success_code Successful response code (:success) + # @option options [Symbol] :failure_code Failure response code (:unauthorized) + def self.should_allow_key_based_auth(http_method, url, parameters={}, options={}) + success_code = options[:success_code] || :success + failure_code = options[:failure_code] || :unauthorized + + context "should allow key based auth using key=X for #{http_method} #{url}" do + context "with a valid api token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + # Simple url parse to add on ?key= or &key= + request_url = if url.match(/\?/) + url + "&key=#{@token.value}" + else + url + "?key=#{@token.value}" + end + send(http_method, request_url, parameters) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + + context "with an invalid api token" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'feeds') + # Simple url parse to add on ?key= or &key= + request_url = if url.match(/\?/) + url + "&key=#{@token.value}" + else + url + "?key=#{@token.value}" + end + send(http_method, request_url, parameters) + end + should_respond_with failure_code + should_respond_with_content_type_based_on_url(url) + should "not login as the user" do + assert_equal User.anonymous, User.current + end + end end - - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "include_www_authenticate_header" do - assert @controller.response.headers.has_key?('WWW-Authenticate') + + context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do + setup do + @user = User.generate! do |user| + user.admin = true + end + @token = Token.create!(:user => @user, :action => 'api') + send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s}) + end + should_respond_with success_code + should_respond_with_content_type_based_on_url(url) + should_be_a_valid_response_string_based_on_url(url) + should "login as the user" do + assert_equal @user, User.current + end + end + end + + # Uses should_respond_with_content_type based on what's in the url: + # + # '/project/issues.xml' => should_respond_with_content_type :xml + # '/project/issues.json' => should_respond_with_content_type :json + # + # @param [String] url Request + def self.should_respond_with_content_type_based_on_url(url) + case + when url.match(/xml/i) + should "respond with XML" do + assert_equal 'application/xml', @response.content_type + end + when url.match(/json/i) + should "respond with JSON" do + assert_equal 'application/json', @response.content_type + end + else + raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}" + end + end + + # Uses the url to assert which format the response should be in + # + # '/project/issues.xml' => should_be_a_valid_xml_string + # '/project/issues.json' => should_be_a_valid_json_string + # + # @param [String] url Request + def self.should_be_a_valid_response_string_based_on_url(url) + case + when url.match(/xml/i) + should_be_a_valid_xml_string + when url.match(/json/i) + should_be_a_valid_json_string + else + raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}" + end + end + + # Checks that the response is a valid JSON string + def self.should_be_a_valid_json_string + should "be a valid JSON string (or empty)" do + assert(response.body.blank? || ActiveSupport::JSON.decode(response.body)) + end + end + + # Checks that the response is a valid XML string + def self.should_be_a_valid_xml_string + should "be a valid XML string" do + assert REXML::Document.new(response.body) + end + end + + def self.should_respond_with(status) + should "respond with #{status}" do + assert_response status end end end end +end - # Test that a request allows the API key with HTTP BASIC - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={}) - success_code = options[:success_code] || :success - failure_code = options[:failure_code] || :unauthorized - - context "should allow http basic auth with a key for #{http_method} #{url}" do - context "with a valid HTTP authentication using the API token" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'api') - send(http_method, url, parameters, credentials(@token.value, 'X')) - end - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should_be_a_valid_response_string_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - - context "with an invalid HTTP authentication" do - setup do - @user = User.generate! - @token = Token.create!(:user => @user, :action => 'feeds') - send(http_method, url, parameters, credentials(@token.value, 'X')) - end - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "not login as the user" do - assert_equal User.anonymous, User.current - end - end - end - end - - # Test that a request allows full key authentication - # - # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete) - # @param [String] url the request url, without the key=ZXY parameter - # @param [optional, Hash] parameters additional request parameters - # @param [optional, Hash] options additional options - # @option options [Symbol] :success_code Successful response code (:success) - # @option options [Symbol] :failure_code Failure response code (:unauthorized) - def self.should_allow_key_based_auth(http_method, url, parameters={}, options={}) - success_code = options[:success_code] || :success - failure_code = options[:failure_code] || :unauthorized - - context "should allow key based auth using key=X for #{http_method} #{url}" do - context "with a valid api token" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'api') - # Simple url parse to add on ?key= or &key= - request_url = if url.match(/\?/) - url + "&key=#{@token.value}" - else - url + "?key=#{@token.value}" - end - send(http_method, request_url, parameters) - end - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should_be_a_valid_response_string_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - - context "with an invalid api token" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'feeds') - # Simple url parse to add on ?key= or &key= - request_url = if url.match(/\?/) - url + "&key=#{@token.value}" - else - url + "?key=#{@token.value}" - end - send(http_method, request_url, parameters) - end - should_respond_with failure_code - should_respond_with_content_type_based_on_url(url) - should "not login as the user" do - assert_equal User.anonymous, User.current - end - end - end - - context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do - setup do - @user = User.generate! do |user| - user.admin = true - end - @token = Token.create!(:user => @user, :action => 'api') - send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s}) - end - should_respond_with success_code - should_respond_with_content_type_based_on_url(url) - should_be_a_valid_response_string_based_on_url(url) - should "login as the user" do - assert_equal @user, User.current - end - end - end - - # Uses should_respond_with_content_type based on what's in the url: - # - # '/project/issues.xml' => should_respond_with_content_type :xml - # '/project/issues.json' => should_respond_with_content_type :json - # - # @param [String] url Request - def self.should_respond_with_content_type_based_on_url(url) - case - when url.match(/xml/i) - should "respond with XML" do - assert_equal 'application/xml', @response.content_type - end - when url.match(/json/i) - should "respond with JSON" do - assert_equal 'application/json', @response.content_type - end - else - raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}" - end - end - - # Uses the url to assert which format the response should be in - # - # '/project/issues.xml' => should_be_a_valid_xml_string - # '/project/issues.json' => should_be_a_valid_json_string - # - # @param [String] url Request - def self.should_be_a_valid_response_string_based_on_url(url) - case - when url.match(/xml/i) - should_be_a_valid_xml_string - when url.match(/json/i) - should_be_a_valid_json_string - else - raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}" - end - end - - # Checks that the response is a valid JSON string - def self.should_be_a_valid_json_string - should "be a valid JSON string (or empty)" do - assert(response.body.blank? || ActiveSupport::JSON.decode(response.body)) - end - end - - # Checks that the response is a valid XML string - def self.should_be_a_valid_xml_string - should "be a valid XML string" do - assert REXML::Document.new(response.body) - end - end - - def self.should_respond_with(status) - should "respond with #{status}" do - assert_response status - end +# URL helpers do not work with config.threadsafe! +# https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454 +ActionView::TestCase::TestController.instance_eval do + helper Rails.application.routes.url_helpers +end +ActionView::TestCase::TestController.class_eval do + def _routes + Rails.application.routes end end - -# Simple module to "namespace" all of the API tests -module ApiTest -end diff -r 038ba2d95de8 -r 261b3d9a4903 test/ui/base.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ui/base.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,72 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'capybara/rails' + +Capybara.default_driver = :selenium +Capybara.register_driver :selenium do |app| + # Use the following driver definition to test locally using Chrome + # (also requires chromedriver to be in PATH) + # Capybara::Selenium::Driver.new(app, :browser => :chrome) + # Add :switches => %w[--lang=en] to force default browser locale to English + # Default for Selenium remote driver is to connect to local host on port 4444 + # This can be change using :url => 'http://localhost:9195' if necessary + # PhantomJS 1.8 now directly supports Webdriver Wire API, + # simply run it with `phantomjs --webdriver 4444` + # Add :desired_capabilities => Selenium::WebDriver::Remote::Capabilities.internet_explorer) + # to run on Selenium Grid Hub with IE + Capybara::Selenium::Driver.new(app, :browser => :remote) +end + +# default: 2 +Capybara.default_wait_time = 2 + +DatabaseCleaner.strategy = :truncation + +module Redmine + module UiTest + # Base class for UI tests + class Base < ActionDispatch::IntegrationTest + include Capybara::DSL + + # Stop ActiveRecord from wrapping tests in transactions + # Transactional fixtures do not work with Selenium tests, because Capybara + # uses a separate server thread, which the transactions would be hidden + self.use_transactional_fixtures = false + + # Should not depend on locale since Redmine displays login page + # using default browser locale which depend on system locale for "real" browsers drivers + def log_user(login, password) + visit '/my/page' + assert_equal '/login', current_path + within('#login-form form') do + fill_in 'username', :with => login + fill_in 'password', :with => password + find('input[name=login]').click + end + assert_equal '/my/page', current_path + end + + teardown do + Capybara.reset_sessions! # Forget the (simulated) browser state + Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver + DatabaseCleaner.clean + end + end + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 test/ui/issues_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ui/issues_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,264 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../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 038ba2d95de8 -r 261b3d9a4903 test/unit/activity_test.rb --- a/test/unit/activity_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/activity_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,7 +19,8 @@ class ActivityTest < ActiveSupport::TestCase fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, - :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :time_entries, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions def setup @project = Project.find(1) @@ -87,6 +88,39 @@ assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort end + def test_event_group_for_issue + issue = Issue.find(1) + assert_equal issue, issue.event_group + end + + def test_event_group_for_journal + issue = Issue.find(1) + journal = issue.journals.first + assert_equal issue, journal.event_group + end + + def test_event_group_for_issue_time_entry + time = TimeEntry.where(:issue_id => 1).first + assert_equal time.issue, time.event_group + end + + def test_event_group_for_project_time_entry + time = TimeEntry.where(:issue_id => nil).first + assert_equal time, time.event_group + end + + def test_event_group_for_message + message = Message.find(1) + reply = message.children.first + assert_equal message, message.event_group + assert_equal message, reply.event_group + end + + def test_event_group_for_wiki_content_version + content = WikiContent::Version.find(1) + assert_equal content.page, content.event_group + end + private def find_events(user, options={}) diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/attachment_test.rb --- a/test/unit/attachment_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/attachment_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -42,6 +42,13 @@ assert_nil Attachment.new.container end + def test_filename_should_remove_eols + assert_equal "line_feed", Attachment.new(:filename => "line\nfeed").filename + assert_equal "line_feed", Attachment.new(:filename => "some\npath/line\nfeed").filename + assert_equal "carriage_return", Attachment.new(:filename => "carriage\rreturn").filename + assert_equal "carriage_return", Attachment.new(:filename => "some\rpath/carriage\rreturn").filename + end + def test_create a = Attachment.new(:container => Issue.find(1), :file => uploaded_test_file("testfile.txt", "text/plain"), @@ -52,10 +59,25 @@ assert_equal 'text/plain', a.content_type assert_equal 0, a.downloads assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest + + assert a.disk_directory + assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory + assert File.exist?(a.diskfile) assert_equal 59, File.size(a.diskfile) end + def test_copy_should_preserve_attributes + a = Attachment.find(1) + copy = a.copy + + assert_save copy + copy = Attachment.order('id DESC').first + %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute| + assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different" + end + end + def test_size_should_be_validated_for_new_file with_settings :attachment_max_size => 0 do a = Attachment.new(:container => Issue.find(1), @@ -78,7 +100,7 @@ def test_description_length_should_be_validated a = Attachment.new(:description => 'a' * 300) assert !a.save - assert_not_nil a.errors[:description] + assert_not_equal [], a.errors[:description] end def test_destroy @@ -168,42 +190,55 @@ 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 + def test_move_from_root_to_target_directory_should_move_root_files + a = Attachment.find(20) + assert a.disk_directory.blank? + # Create a real file for this fixture + File.open(a.diskfile, "w") do |f| + f.write "test file at the root of files directory" + end + assert a.readable? + Attachment.move_from_root_to_target_directory - 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) + a.reload + assert_equal '2012/05', a.disk_directory + assert a.readable? + end + + test "Attachmnet.attach_files 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 - 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'} - }) + 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 - 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 + test "Attachmnet.attach_files 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/auth_source_ldap_test.rb --- a/test/unit/auth_source_ldap_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/auth_source_ldap_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -58,61 +58,48 @@ end if ldap_configured? - context '#authenticate' do - setup do - @auth = AuthSourceLdap.find(1) - @auth.update_attribute :onthefly_register, true + 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 - 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 + test '#authenticate with an invalid LDAP user should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('nouser','123456') + end - context 'with an invalid LDAP user' do - should 'return nil' do - assert_equal nil, @auth.authenticate('nouser','123456') - end - end + test '#authenticate without a login should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('','123456') + end - context 'without a login' do - should 'return nil' do - assert_equal nil, @auth.authenticate('','123456') - end - end + test '#authenticate without a password should return nil' do + auth = AuthSourceLdap.find(1) + assert_equal nil, auth.authenticate('edavis','') + end - context 'without a password' do - should 'return nil' do - assert_equal nil, @auth.authenticate('edavis','') - end - 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 - context 'without filter' do - should 'return any user' do - assert @auth.authenticate('example1','123456') - assert @auth.authenticate('edavis', '123456') - end - end + test '#authenticate with filter should return user who matches the filter only' do + auth = AuthSourceLdap.find(1) + auth.filter = "(mail=*@redmine.org)" - 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 + assert auth.authenticate('example1','123456') + assert_nil auth.authenticate('edavis', '123456') end def test_authenticate_should_timeout @@ -124,6 +111,30 @@ 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/board_test.rb --- a/test/unit/board_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/board_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -100,7 +100,7 @@ end end end - assert_equal 0, Message.count(:conditions => {:board_id => 1}) + assert_equal 0, Message.where(:board_id => 1).count end def test_destroy_should_nullify_children diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/changeset_test.rb --- a/test/unit/changeset_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/changeset_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -30,11 +30,8 @@ def test_ref_keywords_any ActionMailer::Base.deliveries.clear - Setting.commit_fix_status_id = IssueStatus.find( - :first, :conditions => ["is_closed = ?", true]).id - Setting.commit_fix_done_ratio = '90' Setting.commit_ref_keywords = '*' - Setting.commit_fix_keywords = 'fixes , closes' + 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, @@ -50,7 +47,7 @@ def test_ref_keywords Setting.commit_ref_keywords = 'refs' - Setting.commit_fix_keywords = '' + Setting.commit_update_keywords = '' c = Changeset.new(:repository => Project.find(1).repository, :committed_on => Time.now, :comments => 'Ignores #2. Refs #1', @@ -61,7 +58,7 @@ def test_ref_keywords_any_only Setting.commit_ref_keywords = '*' - Setting.commit_fix_keywords = '' + Setting.commit_update_keywords = '' c = Changeset.new(:repository => Project.find(1).repository, :committed_on => Time.now, :comments => 'Ignores #2. Refs #1', @@ -113,10 +110,8 @@ end def test_ref_keywords_closing_with_timelog - Setting.commit_fix_status_id = IssueStatus.find( - :first, :conditions => ["is_closed = ?", true]).id Setting.commit_ref_keywords = '*' - Setting.commit_fix_keywords = 'fixes , closes' + 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, @@ -165,6 +160,48 @@ 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, @@ -176,7 +213,7 @@ end def test_commit_closing_a_subproject_issue - with_settings :commit_fix_status_id => 5, :commit_fix_keywords => 'closes', + with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}], :default_language => 'en' do issue = Issue.find(5) assert !issue.closed? @@ -238,6 +275,28 @@ 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/comment_test.rb --- a/test/unit/comment_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/comment_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/custom_field_test.rb --- a/test/unit/custom_field_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/custom_field_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -57,6 +57,20 @@ 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' @@ -209,6 +223,24 @@ 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 @@ -223,4 +255,42 @@ 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 038ba2d95de8 -r 261b3d9a4903 test/unit/custom_field_user_format_test.rb --- a/test/unit/custom_field_user_format_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/custom_field_user_format_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/custom_field_version_format_test.rb --- a/test/unit/custom_field_version_format_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/custom_field_version_format_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/custom_value_test.rb --- a/test/unit/custom_value_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/custom_value_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/default_data_test.rb --- a/test/unit/default_data_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/default_data_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/document_category_test.rb --- a/test/unit/document_category_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/document_category_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,14 +34,14 @@ end def test_default - assert_nil DocumentCategory.find(:first, :conditions => { :is_default => true }) + assert_nil DocumentCategory.where(:is_default => true).first e = Enumeration.find_by_name('Technical documentation') e.update_attributes(:is_default => true) assert_equal 3, DocumentCategory.default.id end def test_force_default - assert_nil DocumentCategory.find(:first, :conditions => { :is_default => true }) + assert_nil DocumentCategory.where(:is_default => true).first assert_equal 1, DocumentCategory.default.id end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/document_test.rb --- a/test/unit/document_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/document_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/enabled_module_test.rb --- a/test/unit/enabled_module_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/enabled_module_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/enumeration_test.rb --- a/test/unit/enumeration_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/enumeration_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -86,7 +86,7 @@ def test_destroy_with_reassign Enumeration.find(4).destroy(Enumeration.find(6)) - assert_nil Issue.find(:first, :conditions => {:priority_id => 4}) + assert_nil Issue.where(:priority_id => 4).first assert_equal 6, Enumeration.find(6).objects_count end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/group_test.rb --- a/test/unit/group_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/group_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,13 +19,11 @@ class GroupTest < ActiveSupport::TestCase fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, + :enumerations, :users, :projects_trackers, :roles, :member_roles, :members, - :enabled_modules, - :workflows, :groups_users include Redmine::I18n @@ -37,6 +35,14 @@ 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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/activities_helper_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/helpers/activities_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,102 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ActivitiesHelperTest < ActionView::TestCase + include ActivitiesHelper + include Redmine::I18n + + class MockEvent + attr_reader :event_datetime, :event_group, :name + + def initialize(group=nil) + @@count ||= 0 + @name = "e#{@@count}" + @event_datetime = Time.now + @@count.hours + @event_group = group || self + @@count += 1 + end + + def self.clear + @@count = 0 + end + end + + def setup + MockEvent.clear + end + + def test_sort_activity_events_should_sort_by_datetime + events = [] + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new + + assert_equal [ + ['e2', false], + ['e1', false], + ['e0', false] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_should_group_events + events = [] + events << MockEvent.new + events << MockEvent.new(events[0]) + events << MockEvent.new(events[0]) + + assert_equal [ + ['e2', false], + ['e1', true], + ['e0', true] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_with_group_not_in_set_should_group_events + e = MockEvent.new + events = [] + events << MockEvent.new(e) + events << MockEvent.new(e) + + assert_equal [ + ['e2', false], + ['e1', true] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_should_sort_by_datetime_and_group + events = [] + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new(events[1]) + events << MockEvent.new(events[2]) + events << MockEvent.new + events << MockEvent.new(events[2]) + + assert_equal [ + ['e6', false], + ['e4', true], + ['e2', true], + ['e5', false], + ['e3', false], + ['e1', true], + ['e0', false] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/application_helper_test.rb --- a/test/unit/helpers/application_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/application_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,7 +20,9 @@ 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, @@ -32,26 +34,30 @@ 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 - context "#link_to_if_authorized" do - context "authorized user" do - should "be tested" - 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') - context "unauthorized user" do - should "be tested" - end + @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 - should "allow using the :controller and :action for the target link" do - User.current = User.find_by_login('admin') + 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? - @project = Issue.first.project # Used by helper - response = link_to_if_authorized("By controller/action", - {:controller => 'issues', :action => 'edit', :id => Issue.first.id}) - assert_match /href/, response - end - + response = link_to_if_authorized('Never displayed', + {:controller => 'issues', :action => 'show', :id => issue}) + assert_nil response end def test_auto_links @@ -83,7 +89,11 @@ # escaping 'http://foo"bar' => 'http://foo"bar', # wrap in angle brackets - '' => '<http://foo.bar>' + '' => '<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 @@ -91,7 +101,8 @@ if 'ruby'.respond_to?(:encoding) def test_auto_links_with_non_ascii_characters to_test = { - 'http://foo.bar/теÑÑ‚' => 'http://foo.bar/теÑÑ‚' + "http://foo.bar/#{@russian_test}" => + %|http://foo.bar/#{@russian_test}| } to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } end @@ -100,8 +111,11 @@ end def test_auto_mailto - assert_equal '

    ', - textilizable('test@foo.bar') + to_test = { + 'test@foo.bar' => '', + 'test@www.foo.bar' => '', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } end def test_inline_images @@ -131,14 +145,14 @@ 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', + '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', + '!logo.gif!:http://foo.bar/' => 'This is a logo', } - attachments = Attachment.find(:all) + attachments = Attachment.all to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } end @@ -182,13 +196,13 @@ to_test = { 'Inline image: !testtest.jpg!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !testtest.jpeg!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !testtest.jpe!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !testtest.bmp!' => - 'Inline image: ', + 'Inline image: ', } attachments = [a1, a2, a3, a4] @@ -211,9 +225,9 @@ to_test = { 'Inline image: !testfile.png!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !Testfile.PNG!' => - 'Inline image: ', + 'Inline image: ', } attachments = [a1, a2] to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } @@ -242,7 +256,8 @@ if 'ruby'.respond_to?(:encoding) def test_textile_external_links_with_non_ascii_characters to_test = { - 'This is a "link":http://foo.bar/теÑÑ‚' => 'This is a link' + %|This is a "link":http://foo.bar/#{@russian_test}| => + %|This is a link| } to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } end @@ -252,15 +267,21 @@ def test_redmine_links issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, - :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)') - note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'}, - :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)') + :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)') - changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, - :class => 'changeset', :title => 'My very first commit') - changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + 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') @@ -279,25 +300,28 @@ 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_link, + '#3#note-14' => note_link2, # should not ignore leading zero '#03' => '#03', # changesets - 'r1' => changeset_link, - 'r1.' => "#{changeset_link}.", - 'r1, r2' => "#{changeset_link}, #{changeset_link2}", - 'r1,r2' => "#{changeset_link},#{changeset_link2}", + '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, @@ -314,6 +338,7 @@ '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'), @@ -323,6 +348,7 @@ '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'), @@ -553,9 +579,8 @@ end def test_attachment_links - attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment') to_test = { - 'attachment:error281.txt' => attachment_link + 'attachment:error281.txt' => 'error281.txt' } to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" } end @@ -565,11 +590,12 @@ a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago) a2 = Attachment.generate!(:filename => "test.txt") - assert_equal %(

    test.txt

    ), + assert_equal %(

    test.txt

    ), textilizable('attachment:test.txt', :attachments => [a1, a2]) end def test_wiki_links + russian_eacape = CGI.escape(@russian_test) to_test = { '[[CookBook documentation]]' => 'CookBook documentation', '[[Another page|Page]]' => 'Page', @@ -580,7 +606,8 @@ '[[CookBook documentation#One-section]]' => 'CookBook documentation', '[[Another page#anchor|Page]]' => 'Page', # UTF8 anchor - '[[Another_page#ТеÑÑ‚|ТеÑÑ‚]]' => %|ТеÑÑ‚|, + "[[Another_page##{@russian_test}|#{@russian_test}]]" => + %|#{@russian_test}|, # page that doesn't exist '[[Unknown page]]' => 'Unknown page', '[[Unknown page|404]]' => '404', @@ -741,7 +768,7 @@ expected = <<-EXPECTED

    CookBook documentation

    -

    #1

    +

    #1

     [[CookBook documentation]]
     
    @@ -989,14 +1016,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

    '), @@ -1076,6 +1103,26 @@ 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') + assert_equal 'logo.gif', + 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), @@ -1088,6 +1135,17 @@ 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" @@ -1164,18 +1222,47 @@ 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 end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/custom_fields_helper_test.rb --- a/test/unit/helpers/custom_fields_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/custom_fields_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -41,7 +41,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 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/issues_helper_test.rb --- a/test/unit/helpers/issues_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/issues_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,6 +19,7 @@ class IssuesHelperTest < ActionView::TestCase include ApplicationHelper + include Redmine::I18n include IssuesHelper include CustomFieldsHelper include ERB::Util @@ -30,7 +31,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :custom_fields, :attachments, :versions @@ -46,182 +46,226 @@ 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 => 'label_precedes', + :value => 1) + assert_equal "Precedes Bug #1: Can't print recipes added", show_detail(detail, true) + assert_match %r{Precedes Bug #1: Can't 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 => 'label_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 => 'label_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 => 'label_precedes', + :old_value => 1) + assert_equal "Precedes deleted (Bug #1: Can't print recipes)", show_detail(detail, true) + assert_match %r{Precedes deleted \(Bug #1: Can't 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 => 'label_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 => 'label_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 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/projects_helper_test.rb --- a/test/unit/helpers/projects_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/projects_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/queries_helper_test.rb --- a/test/unit/helpers/queries_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/queries_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,37 +29,11 @@ :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] end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/search_helper_test.rb --- a/test/unit/helpers/search_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/search_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,6 +21,7 @@ class SearchHelperTest < ActionView::TestCase include SearchHelper + include Redmine::I18n include ERB::Util def test_highlight_single_token diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/sort_helper_test.rb --- a/test/unit/helpers/sort_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/sort_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/helpers/timelog_helper_test.rb --- a/test/unit/helpers/timelog_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/helpers/timelog_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,69 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class WatchersHelperTest < ActionView::TestCase + include WatchersHelper + include Redmine::I18n + + 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 + + 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 038ba2d95de8 -r 261b3d9a4903 test/unit/initializers/patches_test.rb --- a/test/unit/initializers/patches_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/initializers/patches_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,21 +20,19 @@ class PatchesTest < ActiveSupport::TestCase include Redmine::I18n - context "ActiveRecord::Base.human_attribute_name" do - setup do - Setting.default_language = 'en' - end + def setup + 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 + 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 - 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 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 + 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 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_category_test.rb --- a/test/unit/issue_category_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_category_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,42 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_nested_set_test.rb --- a/test/unit/issue_nested_set_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_nested_set_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,14 +18,11 @@ 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_create_root_issue issue1 = Issue.generate! @@ -60,7 +57,7 @@ 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 @@ -166,23 +163,42 @@ 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 @@ -321,6 +337,17 @@ 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 = Issue.generate!(:parent_issue_id => parent.id) + issue2 = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 0) + assert_equal 0, parent.reload.done_ratio + issue1.reload.update_attribute :status_id, 5 + assert_equal 50, parent.reload.done_ratio + issue2.reload.update_attribute :status_id, 5 + 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) @@ -367,7 +394,7 @@ 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 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_priority_test.rb --- a/test/unit/issue_priority_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_priority_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_relation_test.rb --- a/test/unit/issue_relation_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_relation_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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,51 @@ :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 'label_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 'label_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) + id = relation.id + 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 'label_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 'label_blocked_by', 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 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_status_test.rb --- a/test/unit/issue_status_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_status_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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,7 +98,7 @@ 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 @@ -107,7 +107,7 @@ 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).all assert_equal [50], issues.map {|issue| issue.read_attribute(:done_ratio)}.uniq end end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_test.rb --- a/test/unit/issue_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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') @@ -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} @@ -300,6 +343,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 +455,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 +490,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 +509,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,13 +537,33 @@ :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] + + expected_statuses = [issue.status] + WorkflowTransition.find_all_by_old_status_id( issue.status_id).map(&:new_status).uniq.sort assert_equal expected_statuses, issue.new_statuses_allowed_to(admin) @@ -802,6 +890,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 +1007,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 +1026,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 +1045,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 +1053,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 +1074,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 +1103,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 +1241,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 @@ -1370,6 +1495,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 +1510,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 +1529,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 +1553,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 @@ -1532,6 +1700,19 @@ assert_equal 1, ActionMailer::Base.deliveries.size 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 ActionMailer::Base.deliveries.clear issue = Issue.find(1) @@ -1630,7 +1811,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 +1820,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 +1849,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), @@ -1681,7 +1992,7 @@ :issue_to => Issue.find(7), :relation_type => IssueRelation::TYPE_PRECEDES) IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id]) - + assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort end @@ -1701,7 +2012,7 @@ :issue_to => Issue.find(7), :relation_type => IssueRelation::TYPE_RELATES) IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id]) - + r = IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(7), :relation_type => IssueRelation::TYPE_RELATES) @@ -1710,121 +2021,89 @@ 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 +2111,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 +2134,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 +2189,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 +2202,18 @@ assert_include 'priority-highest', classes end + def test_css_classes_should_include_user_assignment + issue = Issue.generate(:assigned_to_id => 2) + assert_include 'assigned-to-me', issue.css_classes(User.find(2)) + assert_not_include 'assigned-to-me', issue.css_classes(User.find(3)) + end + + def test_css_classes_should_include_user_group_assignment + issue = Issue.generate(:assigned_to_id => 10) + assert_include 'assigned-to-my-group', issue.css_classes(Group.find(10).users.first) + assert_not_include 'assigned-to-my-group', issue.css_classes(User.find(3)) + end + def test_save_attachments_with_hash_should_save_attachments_in_keys_order set_tmp_attachments_directory issue = Issue.generate! @@ -1936,4 +2227,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 038ba2d95de8 -r 261b3d9a4903 test/unit/issue_transaction_test.rb --- a/test/unit/issue_transaction_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/issue_transaction_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/journal_observer_test.rb --- a/test/unit/journal_observer_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/journal_observer_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/journal_test.rb --- a/test/unit/journal_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/journal_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/access_control_test.rb --- a/test/unit/lib/redmine/access_control_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/access_control_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/ciphering_test.rb --- a/test/unit/lib/redmine/ciphering_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/ciphering_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/codeset_util_test.rb --- a/test/unit/lib/redmine/codeset_util_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/codeset_util_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/configuration_test.rb --- a/test/unit/lib/redmine/configuration_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/configuration_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/export/pdf_test.rb --- a/test/unit/lib/redmine/export/pdf_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/export/pdf_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/helpers/calendar_test.rb --- a/test/unit/lib/redmine/helpers/calendar_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/helpers/calendar_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,25 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class DiffTest < ActiveSupport::TestCase + def test_diff + diff = Redmine::Helpers::Diff.new("foo", "bar") + assert_not_nil diff + end +end diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/helpers/gantt_test.rb --- a/test/unit/lib/redmine/helpers/gantt_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/helpers/gantt_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -26,7 +26,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :versions, :groups_users @@ -34,6 +33,7 @@ include ProjectsHelper include IssuesHelper include ERB::Util + include Rails.application.routes.url_helpers def setup setup_with_controller @@ -49,7 +49,7 @@ @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)) @@ -57,11 +57,26 @@ context "#number_of_rows" do context "with one project" do - should "return the number of rows just for that 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 end context "with no project" do - should "return the total number of rows for all the projects, resursively" + 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 end should "not exceed max_rows option" do @@ -746,4 +761,81 @@ context "#to_pdf" do should "be tested" 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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/hook_test.rb --- a/test/unit/lib/redmine/hook_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/hook_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/i18n_test.rb --- a/test/unit/lib/redmine/i18n_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/i18n_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/info_test.rb --- a/test/unit/lib/redmine/info_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/info_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/menu_manager/mapper_test.rb --- a/test/unit/lib/redmine/menu_manager/mapper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/menu_manager/mapper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/menu_manager/menu_helper_test.rb --- a/test/unit/lib/redmine/menu_manager/menu_helper_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/menu_manager/menu_helper_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/menu_manager_test.rb --- a/test/unit/lib/redmine/menu_manager_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/menu_manager_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/mime_type_test.rb --- a/test/unit/lib/redmine/mime_type_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/mime_type_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/notifiable_test.rb --- a/test/unit/lib/redmine/notifiable_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/notifiable_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,34 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class ApplicationHelperTest < 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 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,94 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/plugin_test.rb --- a/test/unit/lib/redmine/plugin_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/plugin_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/safe_attributes_test.rb --- a/test/unit/lib/redmine/safe_attributes_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/safe_attributes_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/darcs_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/filesystem_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/git_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 @@ -396,14 +398,10 @@ 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 end @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb --- a/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,7 +18,7 @@ require File.expand_path('../../../../../../test_helper', __FILE__) begin - require 'mocha' + require 'mocha/setup' class SubversionAdapterTest < ActiveSupport::TestCase diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/themes_test.rb --- a/test/unit/lib/redmine/themes_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/themes_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/unified_diff_test.rb --- a/test/unit/lib/redmine/unified_diff_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/unified_diff_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/utils/date_calculation.rb --- a/test/unit/lib/redmine/utils/date_calculation.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/utils/date_calculation.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/views/builders/json_test.rb --- a/test/unit/lib/redmine/views/builders/json_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/views/builders/json_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/views/builders/xml_test.rb --- a/test/unit/lib/redmine/views/builders/xml_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/views/builders/xml_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/wiki_formatting/macros_test.rb --- a/test/unit/lib/redmine/wiki_formatting/macros_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/wiki_formatting/macros_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb --- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine/wiki_formatting_test.rb --- a/test/unit/lib/redmine/wiki_formatting_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine/wiki_formatting_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/lib/redmine_test.rb --- a/test/unit/lib/redmine_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/lib/redmine_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/mail_handler_test.rb --- a/test/unit/mail_handler_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/mail_handler_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -304,6 +304,51 @@ 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') @@ -325,6 +370,15 @@ assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') 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? + assert_equal 'ecookbook', issue.project.identifier + end + def test_add_issue_with_localized_attributes User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr' issue = submit_email( @@ -447,6 +501,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', @@ -646,73 +715,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 +810,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 @@ -778,6 +840,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 038ba2d95de8 -r 261b3d9a4903 test/unit/mailer_test.rb --- a/test/unit/mailer_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/mailer_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 @@ -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 @@ -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 @@ -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,7 +460,7 @@ 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 @@ -418,7 +476,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| @@ -583,12 +641,66 @@ 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 +712,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 038ba2d95de8 -r 261b3d9a4903 test/unit/member_test.rb --- a/test/unit/member_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/member_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,7 +25,6 @@ :member_roles, :members, :enabled_modules, - :workflows, :groups_users, :watchers, :journals, :journal_details, @@ -122,70 +121,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 038ba2d95de8 -r 261b3d9a4903 test/unit/message_test.rb --- a/test/unit/message_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/message_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/news_test.rb --- a/test/unit/news_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/news_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/principal_test.rb --- a/test/unit/principal_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/principal_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -49,64 +49,60 @@ 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.all.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 038ba2d95de8 -r 261b3d9a4903 test/unit/project_copy_test.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unit/project_copy_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,337 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,263 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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 038ba2d95de8 -r 261b3d9a4903 test/unit/project_nested_set_test.rb --- a/test/unit/project_nested_set_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/project_nested_set_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/project_test.rb --- a/test/unit/project_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/project_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 @@ -155,7 +175,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 +203,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).all.size # some boards assert @ecookbook.boards.any? @@ -191,9 +211,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 @@ -226,7 +246,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 @@ -240,7 +260,7 @@ 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, CustomValue.where(:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']).count end def test_move_an_orphan_project_to_a_root_project @@ -435,56 +455,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 @@ -611,52 +642,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 +721,7 @@ 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 end @@ -707,7 +735,7 @@ 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 +750,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 +765,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 +775,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 +803,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 038ba2d95de8 -r 261b3d9a4903 test/unit/query_test.rb --- a/test/unit/query_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/query_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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') @@ -161,7 +211,7 @@ def test_operator_is_on_float Issue.update_all("estimated_hours = 171.2", "id=2") - query = Query.new(:name => '_') + query = IssueQuery.new(:name => '_') query.add_filter('estimated_hours', '=', ['171.20']) issues = find_issues_with_query(query) assert_equal 1, issues.size @@ -169,12 +219,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 +232,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 +246,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 +259,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 +274,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 +292,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 +314,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 +329,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 +341,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 +368,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 +376,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 +405,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 +420,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 +428,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 +436,36 @@ 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_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_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 +473,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 +499,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 +581,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 +602,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 +617,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 +630,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 +642,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 +652,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 +660,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 +704,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 +715,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 +726,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 +738,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 +755,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 +776,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 +789,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 +802,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 +814,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 +843,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 +908,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 +916,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 +937,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 +968,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 +998,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 +1054,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 +1063,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 +1072,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 +1082,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 +1090,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 +1098,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 +1116,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 +1150,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 +1285,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 +1320,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 +1340,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 +1349,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 +1358,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 +1367,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 +1396,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 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_bazaar_test.rb --- a/test/unit/repository_bazaar_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_bazaar_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -105,7 +105,7 @@ @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.all.each {|c| c.destroy if c.revision.to_i > 2} @project.reload assert_equal 2, @repository.changesets.count diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_cvs_test.rb --- a/test/unit/repository_cvs_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_cvs_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -116,12 +116,12 @@ 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.all.each {|c| c.destroy if c.revision.to_i > 3} @project.reload assert_equal 3, @repository.changesets.count assert_equal %w|3 2 1|, @repository.changesets.all.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) diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_darcs_test.rb --- a/test/unit/repository_darcs_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_darcs_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -79,7 +79,7 @@ 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.all.each {|c| c.destroy if c.revision.to_i > 3} @project.reload assert_equal 3, @repository.changesets.count diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_filesystem_test.rb --- a/test/unit/repository_filesystem_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_filesystem_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_git_test.rb --- a/test/unit/repository_git_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_git_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_mercurial_test.rb --- a/test/unit/repository_mercurial_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_mercurial_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -102,7 +102,7 @@ @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.all.each {|c| c.destroy if c.revision.to_i > 2} @project.reload assert_equal 3, @repository.changesets.count diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_subversion_test.rb --- a/test/unit/repository_subversion_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_subversion_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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,7 +75,7 @@ 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.all.each {|c| c.destroy if c.revision.to_i > 5} @project.reload assert_equal 5, @repository.changesets.count diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/repository_test.rb --- a/test/unit/repository_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/repository_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -182,13 +182,10 @@ 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 +206,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 +275,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', diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/role_test.rb --- a/test/unit/role_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/role_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/search_test.rb --- a/test/unit/search_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/search_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/setting_test.rb --- a/test/unit/setting_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/setting_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -46,16 +46,16 @@ 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 038ba2d95de8 -r 261b3d9a4903 test/unit/time_entry_activity_test.rb --- a/test/unit/time_entry_activity_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/time_entry_activity_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 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 Tue Jan 14 14:37:42 2014 +0000 @@ -0,0 +1,40 @@ +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class 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 038ba2d95de8 -r 261b3d9a4903 test/unit/time_entry_test.rb --- a/test/unit/time_entry_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/time_entry_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/token_test.rb --- a/test/unit/token_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/token_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/user_preference_test.rb --- a/test/unit/user_preference_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/user_preference_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/user_test.rb --- a/test/unit/user_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/user_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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,14 @@ @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.all.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 +70,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") @@ -253,7 +291,7 @@ 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 +302,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! @@ -363,28 +401,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 @@ -459,50 +476,67 @@ 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 end end - def test_lock - user = User.try_to_login("jsmith", "jsmith") - assert_equal @jsmith, user + 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 +614,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 +623,7 @@ end end end - + context "with an unsuccessful authentication" do should "return nil" do assert_nil User.try_to_login('example1', '11111') @@ -646,50 +680,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 +754,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 +872,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 +955,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 +1068,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 diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/version_test.rb --- a/test/unit/version_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/version_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/watcher_test.rb --- a/test/unit/watcher_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/watcher_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/wiki_content_test.rb --- a/test/unit/wiki_content_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/wiki_content_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 @@ -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,7 +117,7 @@ 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? diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/wiki_content_version_test.rb --- a/test/unit/wiki_content_version_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/wiki_content_version_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/wiki_page_test.rb --- a/test/unit/wiki_page_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/wiki_page_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r 038ba2d95de8 -r 261b3d9a4903 test/unit/wiki_redirect_test.rb --- a/test/unit/wiki_redirect_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/wiki_redirect_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/wiki_test.rb --- a/test/unit/wiki_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/wiki_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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 038ba2d95de8 -r 261b3d9a4903 test/unit/workflow_test.rb --- a/test/unit/workflow_test.rb Fri Jun 14 09:05:06 2013 +0100 +++ b/test/unit/workflow_test.rb Tue Jan 14 14:37:42 2014 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2012 Jean-Philippe Lang +# Copyright (C) 2006-2013 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -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